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:
Johann150 2021-09-29 18:44:22 +02:00 committed by GitHub
parent c5e5a9b8ef
commit 414f1d1158
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 51 additions and 17 deletions

View file

@ -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;

View file

@ -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;

11
src/misc/truncate.ts Normal file
View 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);
}
}

View file

@ -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で登録されていたということなので、

View file

@ -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

View file

@ -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,
} }
}, },

View file

@ -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,
}, },