mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:Remove local cover path input & replace with url from web input, include SSRF request filter
This commit is contained in:
		
							parent
							
								
									05731c9f72
								
							
						
					
					
						commit
						290a377ef9
					
				| @ -7,7 +7,7 @@ | ||||
|         <!-- book cover overlay --> | ||||
|         <div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100"> | ||||
|           <div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" /> | ||||
|           <div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover"> | ||||
|           <div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover"> | ||||
|             <ui-tooltip direction="top" :text="$strings.LabelRemoveCover"> | ||||
|               <span class="material-icons text-2xl">delete</span> | ||||
|             </ui-tooltip> | ||||
| @ -16,15 +16,16 @@ | ||||
|       </div> | ||||
|       <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0"> | ||||
|         <div class="flex items-center"> | ||||
|           <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32"> | ||||
|           <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32"> | ||||
|             <ui-file-input ref="fileInput" @change="fileUploadSelected"> | ||||
|               <span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span> | ||||
|               <span class="material-icons text-2xl inline-block md:!hidden">upload</span> | ||||
|             </ui-file-input> | ||||
|           </div> | ||||
| 
 | ||||
|           <form @submit.prevent="submitForm" class="flex flex-grow"> | ||||
|             <ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" /> | ||||
|             <ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">{{ $strings.ButtonSave }}</ui-btn> | ||||
|             <ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" /> | ||||
|             <ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn> | ||||
|           </form> | ||||
|         </div> | ||||
| 
 | ||||
| @ -64,7 +65,7 @@ | ||||
|     <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> | ||||
|       <p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p> | ||||
|       <template v-for="cover in coversFound"> | ||||
|         <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)"> | ||||
|         <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)"> | ||||
|           <covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|         </div> | ||||
|       </template> | ||||
| @ -165,6 +166,9 @@ export default { | ||||
|     userCanUpload() { | ||||
|       return this.$store.getters['user/getUserCanUpload'] | ||||
|     }, | ||||
|     userCanDelete() { | ||||
|       return this.$store.getters['user/getUserCanDelete'] | ||||
|     }, | ||||
|     userToken() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
|     }, | ||||
| @ -222,71 +226,53 @@ export default { | ||||
|         this.coversFound = [] | ||||
|         this.hasSearched = false | ||||
|       } | ||||
|       this.imageUrl = this.media.coverPath || '' | ||||
|       this.imageUrl = '' | ||||
|       this.searchTitle = this.mediaMetadata.title || '' | ||||
|       this.searchAuthor = this.mediaMetadata.authorName || '' | ||||
|       if (this.isPodcast) this.provider = 'itunes' | ||||
|       else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' | ||||
|     }, | ||||
|     removeCover() { | ||||
|       if (!this.media.coverPath) { | ||||
|         this.imageUrl = '' | ||||
|       if (!this.coverPath) { | ||||
|         return | ||||
|       } | ||||
|       this.updateCover('') | ||||
|       this.isProcessing = true | ||||
|       this.$axios | ||||
|         .$delete(`/api/items/${this.libraryItemId}/cover`) | ||||
|         .then(() => {}) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to remove cover', error) | ||||
|           if (error.response?.data) { | ||||
|             this.$toast.error(error.response.data) | ||||
|           } | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.isProcessing = false | ||||
|         }) | ||||
|     }, | ||||
|     submitForm() { | ||||
|       this.updateCover(this.imageUrl) | ||||
|     }, | ||||
|     async updateCover(cover) { | ||||
|       if (cover === this.coverPath) { | ||||
|         console.warn('Cover has not changed..', cover) | ||||
|       if (!cover.startsWith('http:') && !cover.startsWith('https:')) { | ||||
|         this.$toast.error('Invalid URL') | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       this.isProcessing = true | ||||
|       var success = false | ||||
| 
 | ||||
|       if (!cover) { | ||||
|         // Remove cover | ||||
|         success = await this.$axios | ||||
|           .$delete(`/api/items/${this.libraryItemId}/cover`) | ||||
|           .then(() => true) | ||||
|           .catch((error) => { | ||||
|             console.error('Failed to remove cover', error) | ||||
|             if (error.response && error.response.data) { | ||||
|               this.$toast.error(error.response.data) | ||||
|             } | ||||
|             return false | ||||
|           }) | ||||
|       } else if (cover.startsWith('http:') || cover.startsWith('https:')) { | ||||
|         // Download cover from url and use | ||||
|         success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => { | ||||
|           console.error('Failed to download cover from url', error) | ||||
|           if (error.response && error.response.data) { | ||||
|             this.$toast.error(error.response.data) | ||||
|           } | ||||
|           return false | ||||
|         }) | ||||
|       } else { | ||||
|         // Update local cover url | ||||
|         const updatePayload = { | ||||
|           cover | ||||
|         } | ||||
|         success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => { | ||||
|           console.error('Failed to update', error) | ||||
|           if (error.response && error.response.data) { | ||||
|             this.$toast.error(error.response.data) | ||||
|           } | ||||
|           return false | ||||
|         }) | ||||
|       } | ||||
|       if (success) { | ||||
|       this.$axios | ||||
|         .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) | ||||
|         .then(() => { | ||||
|           this.imageUrl = '' | ||||
|           this.$toast.success('Update Successful') | ||||
|       } else if (this.media.coverPath) { | ||||
|         this.imageUrl = this.media.coverPath | ||||
|       } | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to update cover', error) | ||||
|           this.$toast.error(error.response?.data || 'Failed to update cover') | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.isProcessing = false | ||||
|         }) | ||||
|     }, | ||||
|     getSearchQuery() { | ||||
|       var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` | ||||
| @ -319,7 +305,19 @@ export default { | ||||
|       this.hasSearched = true | ||||
|     }, | ||||
|     setCover(coverFile) { | ||||
|       this.updateCover(coverFile.metadata.path) | ||||
|       this.isProcessing = true | ||||
|       this.$axios | ||||
|         .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) | ||||
|         .then(() => { | ||||
|           this.$toast.success('Update Successful') | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to set local cover', error) | ||||
|           this.$toast.error(error.response?.data || 'Failed to set cover') | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.isProcessing = false | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Stunde", | ||||
|   "LabelIcon": "Symbol", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "In die Titelliste aufnehmen", | ||||
|   "LabelIncomplete": "Unvollständig", | ||||
|   "LabelInProgress": "In Bearbeitung", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Include in Tracklist", | ||||
|   "LabelIncomplete": "Incomplete", | ||||
|   "LabelInProgress": "In Progress", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hora", | ||||
|   "LabelIcon": "Icono", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Incluir en Tracklist", | ||||
|   "LabelIncomplete": "Incompleto", | ||||
|   "LabelInProgress": "En Proceso", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Hôte", | ||||
|   "LabelHour": "Heure", | ||||
|   "LabelIcon": "Icone", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Inclure dans la liste des pistes", | ||||
|   "LabelIncomplete": "Incomplet", | ||||
|   "LabelInProgress": "En cours", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Include in Tracklist", | ||||
|   "LabelIncomplete": "Incomplete", | ||||
|   "LabelInProgress": "In Progress", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Hour", | ||||
|   "LabelIcon": "Icon", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Include in Tracklist", | ||||
|   "LabelIncomplete": "Incomplete", | ||||
|   "LabelInProgress": "In Progress", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Sat", | ||||
|   "LabelIcon": "Ikona", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Dodaj u Tracklist", | ||||
|   "LabelIncomplete": "Nepotpuno", | ||||
|   "LabelInProgress": "U tijeku", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Ora", | ||||
|   "LabelIcon": "Icona", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Includi nella Tracklist", | ||||
|   "LabelIncomplete": "Incompleta", | ||||
|   "LabelInProgress": "In Corso", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Serveris", | ||||
|   "LabelHour": "Valanda", | ||||
|   "LabelIcon": "Piktograma", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Įtraukti į takelių sąrašą", | ||||
|   "LabelIncomplete": "Nebaigta", | ||||
|   "LabelInProgress": "Vyksta", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Uur", | ||||
|   "LabelIcon": "Icoon", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Includeer in tracklijst", | ||||
|   "LabelIncomplete": "Incompleet", | ||||
|   "LabelInProgress": "Bezig", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Tjener", | ||||
|   "LabelHour": "Time", | ||||
|   "LabelIcon": "Ikon", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Inkluder i sporliste", | ||||
|   "LabelIncomplete": "Ufullstendig", | ||||
|   "LabelInProgress": "I gang", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Host", | ||||
|   "LabelHour": "Godzina", | ||||
|   "LabelIcon": "Ikona", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Dołącz do listy odtwarzania", | ||||
|   "LabelIncomplete": "Nieukończone", | ||||
|   "LabelInProgress": "W trakcie", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "Хост", | ||||
|   "LabelHour": "Часы", | ||||
|   "LabelIcon": "Иконка", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "Включать в список воспроизведения", | ||||
|   "LabelIncomplete": "Не завершен", | ||||
|   "LabelInProgress": "В процессе", | ||||
|  | ||||
| @ -266,6 +266,7 @@ | ||||
|   "LabelHost": "主机", | ||||
|   "LabelHour": "小时", | ||||
|   "LabelIcon": "图标", | ||||
|   "LabelImageURLFromTheWeb": "Image URL from the web", | ||||
|   "LabelIncludeInTracklist": "包含在音轨列表中", | ||||
|   "LabelIncomplete": "未听完", | ||||
|   "LabelInProgress": "正在听", | ||||
|  | ||||
							
								
								
									
										32
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										32
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,6 +18,7 @@ | ||||
|         "sequelize": "^6.32.1", | ||||
|         "socket.io": "^4.5.4", | ||||
|         "sqlite3": "^5.1.6", | ||||
|         "ssrf-req-filter": "^1.1.0", | ||||
|         "xml2js": "^0.5.0" | ||||
|       }, | ||||
|       "bin": { | ||||
| @ -2387,6 +2388,22 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ssrf-req-filter": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", | ||||
|       "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", | ||||
|       "dependencies": { | ||||
|         "ipaddr.js": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ssrf-req-filter/node_modules/ipaddr.js": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", | ||||
|       "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", | ||||
|       "engines": { | ||||
|         "node": ">= 10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ssri": { | ||||
|       "version": "8.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", | ||||
| @ -4437,6 +4454,21 @@ | ||||
|         "tar": "^6.1.11" | ||||
|       } | ||||
|     }, | ||||
|     "ssrf-req-filter": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", | ||||
|       "integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==", | ||||
|       "requires": { | ||||
|         "ipaddr.js": "^2.1.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "ipaddr.js": { | ||||
|           "version": "2.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", | ||||
|           "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "ssri": { | ||||
|       "version": "8.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", | ||||
|  | ||||
| @ -39,6 +39,7 @@ | ||||
|     "sequelize": "^6.32.1", | ||||
|     "socket.io": "^4.5.4", | ||||
|     "sqlite3": "^5.1.6", | ||||
|     "ssrf-req-filter": "^1.1.0", | ||||
|     "xml2js": "^0.5.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | ||||
| @ -182,22 +182,22 @@ class LibraryItemController { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     var libraryItem = req.libraryItem | ||||
|     let libraryItem = req.libraryItem | ||||
| 
 | ||||
|     var result = null | ||||
|     if (req.body && req.body.url) { | ||||
|     let result = null | ||||
|     if (req.body?.url) { | ||||
|       Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) | ||||
|       result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url) | ||||
|     } else if (req.files && req.files.cover) { | ||||
|     } else if (req.files?.cover) { | ||||
|       Logger.debug(`[LibraryItemController] Handling uploaded cover`) | ||||
|       result = await CoverManager.uploadCover(libraryItem, req.files.cover) | ||||
|     } else { | ||||
|       return res.status(400).send('Invalid request no file or url') | ||||
|     } | ||||
| 
 | ||||
|     if (result && result.error) { | ||||
|     if (result?.error) { | ||||
|       return res.status(400).send(result.error) | ||||
|     } else if (!result || !result.cover) { | ||||
|     } else if (!result?.cover) { | ||||
|       return res.status(500).send('Unknown error occurred') | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -120,13 +120,16 @@ class CoverManager { | ||||
|       await fs.ensureDir(coverDirPath) | ||||
| 
 | ||||
|       var temppath = Path.posix.join(coverDirPath, 'cover') | ||||
|       var success = await downloadFile(url, temppath).then(() => true).catch((err) => { | ||||
|         Logger.error(`[CoverManager] Download image file failed for "${url}"`, err) | ||||
| 
 | ||||
|       let errorMsg = '' | ||||
|       let success = await downloadFile(url, temppath).then(() => true).catch((err) => { | ||||
|         errorMsg = err.message || 'Unknown error' | ||||
|         Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg) | ||||
|         return false | ||||
|       }) | ||||
|       if (!success) { | ||||
|         return { | ||||
|           error: 'Failed to download image from url' | ||||
|           error: 'Failed to download image from url: ' + errorMsg | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| const fs = require('../libs/fsExtra') | ||||
| const rra = require('../libs/recursiveReaddirAsync') | ||||
| const axios = require('axios') | ||||
| const Path = require('path') | ||||
| const ssrfFilter = require('ssrf-req-filter') | ||||
| const fs = require('../libs/fsExtra') | ||||
| const rra = require('../libs/recursiveReaddirAsync') | ||||
| const Logger = require('../Logger') | ||||
| const { AudioMimeType } = require('./constants') | ||||
| 
 | ||||
| @ -210,7 +211,9 @@ module.exports.downloadFile = (url, filepath) => { | ||||
|       url, | ||||
|       method: 'GET', | ||||
|       responseType: 'stream', | ||||
|       timeout: 30000 | ||||
|       timeout: 30000, | ||||
|       httpAgent: ssrfFilter(url), | ||||
|       httpsAgent: ssrfFilter(url) | ||||
|     }).then((response) => { | ||||
|       const writer = fs.createWriteStream(filepath) | ||||
|       response.data.pipe(writer) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user