| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719 |
- import { isCreatablePage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
- import { SupportedAction } from '~/interfaces/activity';
- import { AttachmentType } from '~/server/interfaces/attachment';
- import loggerFactory from '~/utils/logger';
- /* eslint-disable no-use-before-define */
- const logger = loggerFactory('growi:routes:attachment');
- const { serializePageSecurely } = require('../models/serializers/page-serializer');
- const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
- const ApiResponse = require('../util/apiResponse');
- /**
- * @swagger
- * tags:
- * name: Attachments
- */
- /**
- * @swagger
- *
- * components:
- * schemas:
- * Attachment:
- * description: Attachment
- * type: object
- * properties:
- * _id:
- * type: string
- * description: attachment ID
- * example: 5e0734e072560e001761fa67
- * __v:
- * type: number
- * description: attachment version
- * example: 0
- * fileFormat:
- * type: string
- * description: file format in MIME
- * example: text/plain
- * fileName:
- * type: string
- * description: file name
- * example: 601b7c59d43a042c0117e08dd37aad0aimage.txt
- * originalName:
- * type: string
- * description: original file name
- * example: file.txt
- * creator:
- * $ref: '#/components/schemas/User'
- * page:
- * type: string
- * description: page ID attached at
- * example: 5e07345972560e001761fa63
- * createdAt:
- * type: string
- * description: date created at
- * example: 2010-01-01T00:00:00.000Z
- * fileSize:
- * type: number
- * description: file size
- * example: 3494332
- * url:
- * type: string
- * description: attachment URL
- * example: http://localhost/files/5e0734e072560e001761fa67
- * filePathProxied:
- * type: string
- * description: file path proxied
- * example: "/attachment/5e0734e072560e001761fa67"
- * downloadPathProxied:
- * type: string
- * description: download path proxied
- * example: "/download/5e0734e072560e001761fa67"
- */
- /**
- * @swagger
- *
- * components:
- * schemas:
- * AttachmentProfile:
- * description: Attachment
- * type: object
- * properties:
- * id:
- * type: string
- * description: attachment ID
- * example: 5e0734e072560e001761fa67
- * _id:
- * type: string
- * description: attachment ID
- * example: 5e0734e072560e001761fa67
- * __v:
- * type: number
- * description: attachment version
- * example: 0
- * fileFormat:
- * type: string
- * description: file format in MIME
- * example: image/png
- * fileName:
- * type: string
- * description: file name
- * example: 601b7c59d43a042c0117e08dd37aad0a.png
- * originalName:
- * type: string
- * description: original file name
- * example: profile.png
- * creator:
- * $ref: '#/components/schemas/User/properties/_id'
- * page:
- * type: string
- * description: page ID attached at
- * example: null
- * createdAt:
- * type: string
- * description: date created at
- * example: 2010-01-01T00:00:00.000Z
- * fileSize:
- * type: number
- * description: file size
- * example: 3494332
- * filePathProxied:
- * type: string
- * description: file path proxied
- * example: "/attachment/5e0734e072560e001761fa67"
- * downloadPathProxied:
- * type: string
- * description: download path proxied
- * example: "/download/5e0734e072560e001761fa67"
- */
- module.exports = function(crowi, app) {
- const Attachment = crowi.model('Attachment');
- const Page = crowi.model('Page');
- const User = crowi.model('User');
- const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
- const { attachmentService, globalNotificationService } = crowi;
- const activityEvent = crowi.event('activity');
- /**
- * Check the user is accessible to the related page
- *
- * @param {User} user
- * @param {Attachment} attachment
- */
- async function isAccessibleByViewer(user, attachment) {
- if (attachment.page != null) {
- // eslint-disable-next-line no-return-await
- return await Page.isAccessiblePageByViewer(attachment.page, user);
- }
- return true;
- }
- /**
- * Check the user is accessible to the related page
- *
- * @param {User} user
- * @param {Attachment} attachment
- */
- async function isDeletableByUser(user, attachment) {
- // deletable if creator is null
- if (attachment.creator == null) {
- return true;
- }
- const ownerId = attachment.creator._id || attachment.creator;
- if (attachment.page == null) { // when profile image
- return user.id === ownerId.toString();
- }
- // eslint-disable-next-line no-return-await
- return await Page.isAccessiblePageByViewer(attachment.page, user);
- }
- /**
- * Common method to response
- *
- * @param {Request} req
- * @param {Response} res
- * @param {User} user
- * @param {Attachment} attachment
- * @param {boolean} forceDownload
- */
- async function responseForAttachment(req, res, attachment, forceDownload) {
- const { fileUploadService } = crowi;
- if (attachment == null) {
- return res.json(ApiResponse.error('attachment not found'));
- }
- const user = req.user;
- const isAccessible = await isAccessibleByViewer(user, attachment);
- if (!isAccessible) {
- return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'. This attachment might belong to other pages.`));
- }
- // add headers before evaluating 'req.fresh'
- setHeaderToRes(res, attachment, forceDownload);
- // return 304 if request is "fresh"
- // see: http://expressjs.com/en/5x/api.html#req.fresh
- if (req.fresh) {
- return res.sendStatus(304);
- }
- if (fileUploadService.canRespond()) {
- return fileUploadService.respond(res, attachment);
- }
- let fileStream;
- try {
- fileStream = await fileUploadService.findDeliveryFile(attachment);
- }
- catch (e) {
- logger.error(e);
- return res.json(ApiResponse.error(e.message));
- }
- const parameters = {
- ip: req.ip,
- endpoint: req.originalUrl,
- action: SupportedAction.ACTION_ATTACHMENT_DOWNLOAD,
- user: req.user?._id,
- snapshot: {
- username: req.user?.username,
- },
- };
- await crowi.activityService.createActivity(parameters);
- return fileStream.pipe(res);
- }
- /**
- * set http response header
- *
- * @param {Response} res
- * @param {Attachment} attachment
- * @param {boolean} forceDownload
- */
- function setHeaderToRes(res, attachment, forceDownload) {
- res.set({
- ETag: `Attachment-${attachment._id}`,
- 'Last-Modified': attachment.createdAt.toUTCString(),
- });
- if (attachment.fileSize) {
- res.set({
- 'Content-Length': attachment.fileSize,
- });
- }
- // download
- if (forceDownload) {
- res.set({
- 'Content-Disposition': `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
- });
- }
- // reference
- else {
- res.set({
- 'Content-Type': attachment.fileFormat,
- // eslint-disable-next-line max-len
- 'Content-Security-Policy': "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
- 'Content-Disposition': `inline;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
- });
- }
- }
- const actions = {};
- const api = {};
- actions.api = api;
- api.download = async function(req, res) {
- const id = req.params.id;
- const attachment = await Attachment.findById(id);
- return responseForAttachment(req, res, attachment, true);
- };
- /**
- * @api {get} /attachments.get get attachments
- * @apiName get
- * @apiGroup Attachment
- *
- * @apiParam {String} id
- */
- api.get = async function(req, res) {
- const id = req.params.id;
- const attachment = await Attachment.findById(id);
- return responseForAttachment(req, res, attachment);
- };
- api.getBrandLogo = async function(req, res) {
- const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
- if (brandLogoAttachment == null) {
- return res.status(404).json(ApiResponse.error('Brand logo does not exist'));
- }
- return responseForAttachment(req, res, brandLogoAttachment);
- };
- /**
- * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
- * @apiName get
- * @apiGroup Attachment
- *
- * @apiParam {String} pageId, fileName
- */
- api.obsoletedGetForMongoDB = async function(req, res) {
- if (crowi.configManager.getConfig('crowi', 'app:fileUploadType') !== 'mongodb') {
- return res.status(400);
- }
- const pageId = req.params.pageId;
- const fileName = req.params.fileName;
- const filePath = `attachment/${pageId}/${fileName}`;
- const attachment = await Attachment.findOne({ filePath });
- return responseForAttachment(req, res, attachment);
- };
- /**
- * @swagger
- *
- * /attachments.limit:
- * get:
- * tags: [Attachments]
- * operationId: getAttachmentsLimit
- * summary: /attachments.limit
- * description: Get available capacity of uploaded file with GridFS
- * parameters:
- * - in: query
- * name: fileSize
- * schema:
- * type: number
- * description: file size
- * example: 23175
- * required: true
- * responses:
- * 200:
- * description: Succeeded to get available capacity of uploaded file with GridFS.
- * content:
- * application/json:
- * schema:
- * properties:
- * isUploadable:
- * type: boolean
- * description: uploadable
- * example: true
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * @api {get} /attachments.limit get available capacity of uploaded file with GridFS
- * @apiName AddAttachments
- * @apiGroup Attachment
- */
- api.limit = async function(req, res) {
- const { fileUploadService } = crowi;
- const fileSize = Number(req.query.fileSize);
- return res.json(ApiResponse.success(await fileUploadService.checkLimit(fileSize)));
- };
- /**
- * @swagger
- *
- * /attachments.add:
- * post:
- * tags: [Attachments, CrowiCompatibles]
- * operationId: addAttachment
- * summary: /attachments.add
- * description: Add attachment to the page
- * requestBody:
- * content:
- * "multipart/form-data":
- * schema:
- * properties:
- * page_id:
- * nullable: true
- * type: string
- * path:
- * nullable: true
- * type: string
- * file:
- * type: string
- * format: binary
- * description: attachment data
- * encoding:
- * path:
- * contentType: application/x-www-form-urlencoded
- * "*\/*":
- * schema:
- * properties:
- * page_id:
- * nullable: true
- * type: string
- * path:
- * nullable: true
- * type: string
- * file:
- * type: string
- * format: binary
- * description: attachment data
- * encoding:
- * path:
- * contentType: application/x-www-form-urlencoded
- * responses:
- * 200:
- * description: Succeeded to add attachment.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * page:
- * $ref: '#/components/schemas/Page'
- * attachment:
- * $ref: '#/components/schemas/Attachment'
- * url:
- * $ref: '#/components/schemas/Attachment/properties/url'
- * pageCreated:
- * type: boolean
- * description: whether the page was created
- * example: false
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * @api {post} /attachments.add Add attachment to the page
- * @apiName AddAttachments
- * @apiGroup Attachment
- *
- * @apiParam {String} page_id
- * @apiParam {File} file
- */
- api.add = async function(req, res) {
- const pageId = req.body.page_id || null;
- const pagePath = req.body.path || null;
- // check params
- if (pageId == null && pagePath == null) {
- return res.json(ApiResponse.error('Either page_id or path is required.'));
- }
- if (req.file == null) {
- return res.json(ApiResponse.error('File error.'));
- }
- const file = req.file;
- try {
- const page = await Page.findById(pageId);
- // check the user is accessible
- const isAccessible = await Page.isAccessiblePageByViewer(page.id, req.user);
- if (!isAccessible) {
- return res.json(ApiResponse.error(`Forbidden to access to the page '${page.id}'`));
- }
- const attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
- const result = {
- page: serializePageSecurely(page),
- revision: serializeRevisionSecurely(page.revision),
- attachment: attachment.toObject({ virtuals: true }),
- };
- activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_ADD });
- res.json(ApiResponse.success(result));
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error(err.message));
- }
- };
- /**
- * @swagger
- *
- * /attachments.uploadProfileImage:
- * post:
- * tags: [Attachments]
- * operationId: uploadProfileImage
- * summary: /attachments.uploadProfileImage
- * description: Upload profile image
- * requestBody:
- * content:
- * "multipart/form-data":
- * schema:
- * properties:
- * file:
- * type: string
- * format: binary
- * description: attachment data
- * user:
- * type: string
- * description: user to set profile image
- * encoding:
- * path:
- * contentType: application/x-www-form-urlencoded
- * "*\/*":
- * schema:
- * properties:
- * file:
- * type: string
- * format: binary
- * description: attachment data
- * user:
- * type: string
- * description: user to set profile
- * encoding:
- * path:
- * contentType: application/x-www-form-urlencoded
- * responses:
- * 200:
- * description: Succeeded to add attachment.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * attachment:
- * $ref: '#/components/schemas/AttachmentProfile'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * @api {post} /attachments.uploadProfileImage Add attachment for profile image
- * @apiName UploadProfileImage
- * @apiGroup Attachment
- *
- * @apiParam {File} file
- */
- api.uploadProfileImage = async function(req, res) {
- // check params
- if (req.file == null) {
- return res.json(ApiResponse.error('File error.'));
- }
- if (!req.user) {
- return res.json(ApiResponse.error('param "user" must be set.'));
- }
- const file = req.file;
- // check type
- const acceptableFileType = /image\/.+/;
- if (!file.mimetype.match(acceptableFileType)) {
- return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
- }
- let attachment;
- try {
- req.user.deleteImage();
- attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.PROFILE_IMAGE);
- await req.user.updateImage(attachment);
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error(err.message));
- }
- const result = {
- attachment: attachment.toObject({ virtuals: true }),
- };
- return res.json(ApiResponse.success(result));
- };
- /**
- * @swagger
- *
- * /attachments.remove:
- * post:
- * tags: [Attachments, CrowiCompatibles]
- * operationId: removeAttachment
- * summary: /attachments.remove
- * description: Remove attachment
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * attachment_id:
- * $ref: '#/components/schemas/Attachment/properties/_id'
- * required:
- * - attachment_id
- * responses:
- * 200:
- * description: Succeeded to remove attachment.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * @api {post} /attachments.remove Remove attachments
- * @apiName RemoveAttachments
- * @apiGroup Attachment
- *
- * @apiParam {String} attachment_id
- */
- api.remove = async function(req, res) {
- const id = req.body.attachment_id;
- const attachment = await Attachment.findById(id);
- if (attachment == null) {
- return res.json(ApiResponse.error('attachment not found'));
- }
- const isDeletable = await isDeletableByUser(req.user, attachment);
- if (!isDeletable) {
- return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
- }
- try {
- await attachmentService.removeAttachment(attachment);
- }
- catch (err) {
- logger.error(err);
- return res.status(500).json(ApiResponse.error('Error while deleting file'));
- }
- activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_REMOVE });
- return res.json(ApiResponse.success({}));
- };
- /**
- * @swagger
- *
- * /attachments.removeProfileImage:
- * post:
- * tags: [Attachments]
- * operationId: removeProfileImage
- * summary: /attachments.removeProfileImage
- * description: Remove profile image
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * user:
- * type: string
- * description: user to remove profile image
- * responses:
- * 200:
- * description: Succeeded to add attachment.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * @api {post} /attachments.removeProfileImage Remove profile image attachments
- * @apiGroup Attachment
- * @apiParam {String} attachment_id
- */
- api.removeProfileImage = async function(req, res) {
- const user = req.user;
- const attachment = await Attachment.findById(user.imageAttachment);
- if (attachment == null) {
- return res.json(ApiResponse.error('attachment not found'));
- }
- const isDeletable = await isDeletableByUser(user, attachment);
- if (!isDeletable) {
- return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
- }
- try {
- await user.deleteImage();
- }
- catch (err) {
- logger.error(err);
- return res.status(500).json(ApiResponse.error('Error while deleting image'));
- }
- return res.json(ApiResponse.success({}));
- };
- return actions;
- };
|