diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 43f59f1e4f..b00bd81655 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -1,2 +1,47 @@ export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days + +// ブラウザで直接表示することを許可するファイルの種類のリスト +// ここに含まれないものは application/octet-stream としてレスポンスされる +// SVGはXSSを生むので許可しない +export const FILE_TYPE_BROWSERSAFE = [ + // Images + 'image/png', + 'image/gif', + 'image/jpeg', + 'image/webp', + 'image/apng', + 'image/bmp', + 'image/tiff', + 'image/x-icon', + + // OggS + 'audio/opus', + 'video/ogg', + 'audio/ogg', + 'application/ogg', + + // ISO/IEC base media file format + 'video/quicktime', + 'video/mp4', + 'audio/mp4', + 'video/x-m4v', + 'audio/x-m4a', + 'video/3gpp', + 'video/3gpp2', + + 'video/mpeg', + 'audio/mpeg', + + 'video/webm', + 'audio/webm', + + 'audio/aac', + 'audio/x-flac', + 'audio/vnd.wave', +]; +/* +https://github.com/sindresorhus/file-type/blob/main/supported.js +https://github.com/sindresorhus/file-type/blob/main/core.js +https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers +*/ diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts index f36aa8f966..361cdd2951 100644 --- a/packages/backend/src/misc/get-file-info.ts +++ b/packages/backend/src/misc/get-file-info.ts @@ -99,7 +99,10 @@ export async function getFileInfo(path: string): Promise { /** * Detect MIME Type and extension */ -export async function detectType(path: string) { +export async function detectType(path: string): Promise<{ + mime: string; + ext: string | null; +}> { // Check 0 byte const fileSize = await getFileSize(path); if (fileSize === 0) { diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 8bb5655b4f..7bfc36e25a 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -14,6 +14,7 @@ import { detectType } from '@/misc/get-file-info'; import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor'; import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail'; import { StatusError } from '@/misc/fetch'; +import { FILE_TYPE_BROWSERSAFE } from '@/const'; //const _filename = fileURLToPath(import.meta.url); const _filename = __filename; @@ -27,6 +28,7 @@ const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => ctx.set('Cache-Control', 'max-age=300'); }; +// eslint-disable-next-line import/no-default-export export default async function(ctx: Koa.Context) { const key = ctx.params.key; @@ -81,7 +83,7 @@ export default async function(ctx: Koa.Context) { const image = await convertFile(); ctx.body = image.data; - ctx.set('Content-Type', image.type); + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); ctx.set('Cache-Control', 'max-age=31536000, immutable'); } catch (e) { serverLogger.error(`${e}`); @@ -112,14 +114,14 @@ export default async function(ctx: Koa.Context) { }).toString(); ctx.body = InternalStorage.read(key); - ctx.set('Content-Type', mime); + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream'); ctx.set('Cache-Control', 'max-age=31536000, immutable'); ctx.set('Content-Disposition', contentDisposition('inline', filename)); } else { const readable = InternalStorage.read(file.accessKey!); readable.on('error', commonReadableHandlerGenerator(ctx)); ctx.body = readable; - ctx.set('Content-Type', file.type); + ctx.set('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream'); ctx.set('Cache-Control', 'max-age=31536000, immutable'); ctx.set('Content-Disposition', contentDisposition('inline', file.name)); } diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts index 9e13c0877f..b116b4b961 100644 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ b/packages/backend/src/server/proxy/proxy-media.ts @@ -6,6 +6,7 @@ import { createTemp } from '@/misc/create-temp'; import { downloadUrl } from '@/misc/download-url'; import { detectType } from '@/misc/get-file-info'; import { StatusError } from '@/misc/fetch'; +import { FILE_TYPE_BROWSERSAFE } from '@/const'; export async function proxyMedia(ctx: Koa.Context) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; @@ -18,7 +19,7 @@ export async function proxyMedia(ctx: Koa.Context) { const { mime, ext } = await detectType(path); - if (!mime.startsWith('image/')) throw 403; + if (!FILE_TYPE_BROWSERSAFE.includes(mime)) throw 403; let image: IImage; diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index ee4d51a96d..a59c9501bc 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -20,6 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error'; import * as S3 from 'aws-sdk/clients/s3'; import { getS3 } from './s3'; import * as sharp from 'sharp'; +import { FILE_TYPE_BROWSERSAFE } from '@/const'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -241,6 +242,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool */ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { if (type === 'image/apng') type = 'image/png'; + if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; const meta = await fetchMeta(); diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index ff450246f9..a8eed1ca21 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -77,7 +77,7 @@ import { defineComponent, markRaw } from 'vue'; import { emojilist } from '@/scripts/emojilist'; import { getStaticImageUrl } from '@/scripts/get-static-image-url'; -import Particle from '@/components/particle.vue'; +import Ripple from '@/components/ripple.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { isMobile } from '@/scripts/is-mobile'; @@ -296,9 +296,9 @@ export default defineComponent({ if (ev) { const el = ev.currentTarget || ev.target; const rect = el.getBoundingClientRect(); - const x = rect.left + (el.clientWidth / 2); - const y = rect.top + (el.clientHeight / 2); - os.popup(Particle, { x, y }, {}, 'end'); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(Ripple, { x, y }, {}, 'end'); } const key = this.getKey(emoji); diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index c990b693f1..3533f4f27b 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -5,7 +5,7 @@
input { - border-color: var(--accent); + border-color: var(--accent) !important; //box-shadow: 0 0 0 4px var(--focus); } } diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue index 9ecff1aa6f..afc53ca9c8 100644 --- a/packages/client/src/components/form/select.vue +++ b/packages/client/src/components/form/select.vue @@ -3,7 +3,9 @@
- - - + + - +

@@ -55,16 +64,7 @@ export default defineComponent({ .ziffeoms { position: relative; display: flex; - cursor: pointer; - transition: all 0.3s; - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } + transition: all 0.2s ease; > * { user-select: none; @@ -80,27 +80,32 @@ export default defineComponent({ > .button { position: relative; - display: inline-block; + display: inline-flex; flex-shrink: 0; margin: 0; - width: 36px; - height: 26px; - background: var(--switchBg); + box-sizing: border-box; + width: 23px; + height: 23px; outline: none; - border-radius: 999px; + background: var(--panel); + border: solid 1px var(--panel); + border-radius: 4px; + cursor: pointer; transition: inherit; - > .handle { - position: absolute; - top: 0; - bottom: 0; - left: 5px; - margin: auto 0; - border-radius: 100%; - transition: background-color 0.3s, transform 0.3s; - width: 16px; - height: 16px; - background-color: #fff; + > .check { + margin: auto; + opacity: 0; + color: var(--fgOnAccent); + font-size: 13px; + transform: scale(0.5); + transition: all 0.2s ease; + } + } + + &:hover { + > .button { + border-color: var(--inputBorderHover) !important; } } @@ -108,13 +113,13 @@ export default defineComponent({ margin-left: 16px; margin-top: 2px; display: block; - cursor: pointer; transition: inherit; color: var(--fg); > span { display: block; line-height: 20px; + cursor: pointer; transition: inherit; } @@ -129,12 +134,6 @@ export default defineComponent({ } } - &:hover { - > .button { - background-color: var(--accentedBg); - } - } - &.disabled { opacity: 0.6; cursor: not-allowed; @@ -142,11 +141,12 @@ export default defineComponent({ &.checked { > .button { - background-color: var(--accent); - border-color: var(--accent); + background-color: var(--accent) !important; + border-color: var(--accent) !important; - > .handle { - transform: translateX(10px); + > .check { + opacity: 1; + transform: scale(1); } } } diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue index 98fd0da94b..c9ba9b97a2 100644 --- a/packages/client/src/components/form/textarea.vue +++ b/packages/client/src/components/form/textarea.vue @@ -4,7 +4,7 @@