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
|
@ -3,10 +3,13 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="fullwidth top-caption">
|
<div class="fullwidth top-caption">
|
||||||
<div class="mk-dialog">
|
<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>
|
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
|
||||||
<div class="buttons" v-if="(showOkButton || showCancelButton)">
|
<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>
|
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,10 +29,12 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { length } from 'stringz';
|
||||||
import MkModal from '@client/components/ui/modal.vue';
|
import MkModal from '@client/components/ui/modal.vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import bytes from '@client/filters/bytes';
|
import bytes from '@client/filters/bytes';
|
||||||
import number from '@client/filters/number';
|
import number from '@client/filters/number';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -79,6 +84,13 @@ export default defineComponent({
|
||||||
document.removeEventListener('keydown', this.onKeydown);
|
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: {
|
methods: {
|
||||||
bytes,
|
bytes,
|
||||||
number,
|
number,
|
||||||
|
@ -156,10 +168,20 @@ export default defineComponent({
|
||||||
|
|
||||||
> header {
|
> header {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> .title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .text-count {
|
||||||
|
opacity: 0.7;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .buttons {
|
> .buttons {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
||||||
|
|
|
@ -6,3 +6,9 @@
|
||||||
* Surrogate pairs count as one
|
* Surrogate pairs count as one
|
||||||
*/
|
*/
|
||||||
export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
|
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;
|
||||||
|
|
|
@ -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 { apLogger } from '../logger';
|
||||||
import { DriveFile } from '@/models/entities/drive-file';
|
import { DriveFile } from '@/models/entities/drive-file';
|
||||||
import { DriveFiles } from '@/models/index';
|
import { DriveFiles } from '@/models/index';
|
||||||
|
import { truncate } from '@/misc/truncate';
|
||||||
|
import { DM_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
|
||||||
|
|
||||||
const logger = apLogger;
|
const logger = apLogger;
|
||||||
|
|
||||||
|
@ -28,7 +30,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
|
||||||
const instance = await fetchMeta();
|
const instance = await fetchMeta();
|
||||||
const cache = instance.cacheRemoteFiles;
|
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) {
|
if (file.isLink) {
|
||||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||||
|
|
|
@ -28,22 +28,13 @@ import { getConnection } from 'typeorm';
|
||||||
import { toArray } from '@/prelude/array';
|
import { toArray } from '@/prelude/array';
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search';
|
import { normalizeForSearch } from '@/misc/normalize-for-search';
|
||||||
|
import { truncate } from '@/misc/truncate';
|
||||||
|
|
||||||
const logger = apLogger;
|
const logger = apLogger;
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
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
|
* Validate and convert to actor object
|
||||||
* @param x Fetched object
|
* @param x Fetched object
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { publishDriveStream } from '@/services/stream';
|
||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
import { ApiError } from '../../../error';
|
import { ApiError } from '../../../error';
|
||||||
import { DriveFiles, DriveFolders } from '@/models/index';
|
import { DriveFiles, DriveFolders } from '@/models/index';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -33,7 +34,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
comment: {
|
comment: {
|
||||||
validator: $.optional.nullable.str,
|
validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||||
default: undefined as any,
|
default: undefined as any,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@ import uploadFromUrl from '@/services/drive/upload-from-url';
|
||||||
import define from '../../../define';
|
import define from '../../../define';
|
||||||
import { DriveFiles } from '@/models/index';
|
import { DriveFiles } from '@/models/index';
|
||||||
import { publishMainStream } from '@/services/stream';
|
import { publishMainStream } from '@/services/stream';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -35,7 +36,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
comment: {
|
comment: {
|
||||||
validator: $.optional.nullable.str,
|
validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue