fix: truncate image descriptions (#7699)
* move truncate function to separate file to reuse it * truncate image descriptions * show image description limit in UI * correctly treat null Co-authored-by: nullobsi <me@nullob.si> * make truncate Unicode-aware The strings that truncate returns should now be valid Unicode. PostgreSQL also counts Unicode Code Points instead of bytes so this should be correct. * move truncate to internal, validate in API Truncating could also be done in src/services/drive/add-file.ts or src/services/drive/upload-from-url.ts but those would also affect local images. But local images should result in a hard error if the image comment is too long. * avoid overwriting Co-authored-by: nullobsi <me@nullob.si>
This commit is contained in:
		
							parent
							
								
									c5e5a9b8ef
								
							
						
					
					
						commit
						414f1d1158
					
				
					 7 changed files with 51 additions and 17 deletions
				
			
		|  | @ -3,10 +3,13 @@ | |||
| 		<div class="container"> | ||||
| 			<div class="fullwidth top-caption"> | ||||
| 				<div class="mk-dialog"> | ||||
| 					<header v-if="title"><Mfm :text="title"/></header> | ||||
| 					<header> | ||||
| 						<Mfm v-if="title" class="title" :text="title"/> | ||||
| 						<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span> | ||||
| 					</header> | ||||
| 					<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea> | ||||
| 					<div class="buttons" v-if="(showOkButton || showCancelButton)"> | ||||
| 						<MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton> | ||||
| 						<MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton> | ||||
| 						<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -26,10 +29,12 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { length } from 'stringz'; | ||||
| import MkModal from '@client/components/ui/modal.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import number from '@client/filters/number'; | ||||
| import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -79,6 +84,13 @@ export default defineComponent({ | |||
| 		document.removeEventListener('keydown', this.onKeydown); | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		remainingLength(): number { | ||||
| 			if (typeof this.inputValue != "string") return DB_MAX_IMAGE_COMMENT_LENGTH; | ||||
| 			return DB_MAX_IMAGE_COMMENT_LENGTH - length(this.inputValue); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		bytes, | ||||
| 		number, | ||||
|  | @ -156,8 +168,18 @@ export default defineComponent({ | |||
| 
 | ||||
| 	> header { | ||||
| 		margin: 0 0 8px 0; | ||||
| 		font-weight: bold; | ||||
| 		font-size: 20px; | ||||
| 		position: relative; | ||||
| 
 | ||||
| 		> .title { | ||||
| 			font-weight: bold; | ||||
| 			font-size: 20px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .text-count { | ||||
| 			opacity: 0.7; | ||||
| 			position: absolute; | ||||
| 			right: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .buttons { | ||||
|  |  | |||
|  | @ -6,3 +6,9 @@ | |||
|  * Surrogate pairs count as one | ||||
|  */ | ||||
| export const DB_MAX_NOTE_TEXT_LENGTH = 8192; | ||||
| 
 | ||||
| /** | ||||
|  * Maximum image description length that can be stored in DB. | ||||
|  * Surrogate pairs count as one | ||||
|  */ | ||||
| export const DB_MAX_IMAGE_COMMENT_LENGTH = 512; | ||||
|  |  | |||
							
								
								
									
										11
									
								
								src/misc/truncate.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/misc/truncate.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| import { substring } from 'stringz'; | ||||
| 
 | ||||
| export function truncate(input: string, size: number): string; | ||||
| export function truncate(input: string | undefined, size: number): string | undefined; | ||||
| export function truncate(input: string | undefined, size: number): string | undefined { | ||||
| 	if (!input) { | ||||
| 		return input; | ||||
| 	} else { | ||||
| 		return substring(input, 0, size); | ||||
| 	} | ||||
| } | ||||
|  | @ -5,6 +5,8 @@ import { fetchMeta } from '@/misc/fetch-meta'; | |||
| import { apLogger } from '../logger'; | ||||
| import { DriveFile } from '@/models/entities/drive-file'; | ||||
| import { DriveFiles } from '@/models/index'; | ||||
| import { truncate } from '@/misc/truncate'; | ||||
| import { DM_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; | ||||
| 
 | ||||
| const logger = apLogger; | ||||
| 
 | ||||
|  | @ -28,7 +30,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive | |||
| 	const instance = await fetchMeta(); | ||||
| 	const cache = instance.cacheRemoteFiles; | ||||
| 
 | ||||
| 	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, image.name); | ||||
| 	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH)); | ||||
| 
 | ||||
| 	if (file.isLink) { | ||||
| 		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
 | ||||
|  |  | |||
|  | @ -28,22 +28,13 @@ import { getConnection } from 'typeorm'; | |||
| import { toArray } from '@/prelude/array'; | ||||
| import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata'; | ||||
| import { normalizeForSearch } from '@/misc/normalize-for-search'; | ||||
| import { truncate } from '@/misc/truncate'; | ||||
| 
 | ||||
| const logger = apLogger; | ||||
| 
 | ||||
| const nameLength = 128; | ||||
| const summaryLength = 2048; | ||||
| 
 | ||||
| function truncate(input: string, size: number): string; | ||||
| function truncate(input: string | undefined, size: number): string | undefined; | ||||
| function truncate(input: string | undefined, size: number): string | undefined { | ||||
| 	if (!input || input.length <= size) { | ||||
| 		return input; | ||||
| 	} else { | ||||
| 		return input.substring(0, size); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Validate and convert to actor object | ||||
|  * @param x Fetched object | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { publishDriveStream } from '@/services/stream'; | |||
| import define from '../../../define'; | ||||
| import { ApiError } from '../../../error'; | ||||
| import { DriveFiles, DriveFolders } from '@/models/index'; | ||||
| import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['drive'], | ||||
|  | @ -33,7 +34,7 @@ export const meta = { | |||
| 		}, | ||||
| 
 | ||||
| 		comment: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), | ||||
| 			default: undefined as any, | ||||
| 		} | ||||
| 	}, | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import uploadFromUrl from '@/services/drive/upload-from-url'; | |||
| import define from '../../../define'; | ||||
| import { DriveFiles } from '@/models/index'; | ||||
| import { publishMainStream } from '@/services/stream'; | ||||
| import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['drive'], | ||||
|  | @ -35,7 +36,7 @@ export const meta = { | |||
| 		}, | ||||
| 
 | ||||
| 		comment: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH), | ||||
| 			default: null, | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue