ogp.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import * as fs from 'fs';
  2. import path from 'path';
  3. import { getIdStringForRef, type IUser } from '@growi/core';
  4. import { DevidedPagePath } from '@growi/core/dist/models';
  5. // eslint-disable-next-line no-restricted-imports
  6. import axios from 'axios';
  7. import type {
  8. Request, Response, NextFunction,
  9. } from 'express';
  10. import type { ValidationError } from 'express-validator';
  11. import { param, validationResult } from 'express-validator';
  12. import type { HydratedDocument } from 'mongoose';
  13. import mongoose from 'mongoose';
  14. import loggerFactory from '~/utils/logger';
  15. import { projectRoot } from '~/utils/project-dir-utils';
  16. import { Attachment } from '../models/attachment';
  17. import type { PageDocument, PageModel } from '../models/page';
  18. import { configManager } from '../service/config-manager';
  19. import { convertStreamToBuffer } from '../util/stream';
  20. const logger = loggerFactory('growi:routes:ogp');
  21. const DEFAULT_USER_IMAGE_URL = '/images/icons/user.svg';
  22. const DEFAULT_USER_IMAGE_PATH = `public${DEFAULT_USER_IMAGE_URL}`;
  23. let bufferedDefaultUserImageCache: Buffer = Buffer.from('');
  24. fs.readFile(path.join(projectRoot, DEFAULT_USER_IMAGE_PATH), (err, buffer) => {
  25. if (err) throw err;
  26. bufferedDefaultUserImageCache = buffer;
  27. });
  28. module.exports = function(crowi) {
  29. const isUserImageAttachment = (userImageUrlCached: string): boolean => {
  30. return /^\/attachment\/.+/.test(userImageUrlCached);
  31. };
  32. const getBufferedUserImage = async(userImageUrlCached: string): Promise<Buffer> => {
  33. let bufferedUserImage: Buffer;
  34. if (isUserImageAttachment(userImageUrlCached)) {
  35. const { fileUploadService } = crowi;
  36. const attachment = await Attachment.findById(userImageUrlCached);
  37. const fileStream = await fileUploadService.findDeliveryFile(attachment);
  38. bufferedUserImage = await convertStreamToBuffer(fileStream);
  39. return bufferedUserImage;
  40. }
  41. return (await axios.get(
  42. userImageUrlCached, {
  43. responseType: 'arraybuffer',
  44. },
  45. )).data;
  46. };
  47. const renderOgp = async(req: Request, res: Response) => {
  48. const ogpUri = configManager.getConfig('app:ogpUri');
  49. if (ogpUri == null) {
  50. return res.status(501).send('OGP_URI for growi-unique-ogp has not been setup');
  51. }
  52. const page: PageDocument = req.body.page; // asserted by ogpValidator
  53. const title = (new DevidedPagePath(page.path)).latter;
  54. let user: IUser | null = null;
  55. let userName = '(unknown)';
  56. let userImage: Buffer = bufferedDefaultUserImageCache;
  57. try {
  58. if (page.creator != null) {
  59. const User = mongoose.model<IUser>('User');
  60. user = await User.findById(getIdStringForRef(page.creator));
  61. if (user != null) {
  62. userName = user.username;
  63. userImage = user.imageUrlCached !== DEFAULT_USER_IMAGE_URL
  64. ? bufferedDefaultUserImageCache
  65. : await getBufferedUserImage(user.imageUrlCached);
  66. }
  67. }
  68. }
  69. catch (err) {
  70. logger.error(err);
  71. return res.status(500).send(`error: ${err}`);
  72. }
  73. let result;
  74. try {
  75. result = await axios.post(
  76. ogpUri, {
  77. data: {
  78. title,
  79. userName,
  80. userImage,
  81. },
  82. }, {
  83. responseType: 'stream',
  84. },
  85. );
  86. }
  87. catch (err) {
  88. logger.error(err);
  89. return res.status(500).send(`error: ${err}`);
  90. }
  91. res.writeHead(200, {
  92. 'Content-Type': 'image/jpeg',
  93. });
  94. result.data.pipe(res);
  95. };
  96. const pageIdRequired = param('pageId').not().isEmpty().withMessage('page id is not included in the parameter');
  97. const ogpValidator = async(req:Request, res:Response, next:NextFunction) => {
  98. const { aclService, fileUploadService, configManager } = crowi;
  99. const ogpUri = configManager.getConfig('app:ogpUri');
  100. if (ogpUri == null) return res.status(400).send('OGP URI for GROWI has not been setup');
  101. if (!fileUploadService.getIsUploadable()) return res.status(501).send('This GROWI can not upload file');
  102. if (!aclService.isGuestAllowedToRead()) return res.status(501).send('This GROWI is not public');
  103. const errors = validationResult(req);
  104. if (errors.isEmpty()) {
  105. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
  106. try {
  107. const page = await Page.findByIdAndViewer(req.params.pageId, null);
  108. if (page == null || page.status !== Page.STATUS_PUBLISHED || (page.grant !== Page.GRANT_PUBLIC && page.grant !== Page.GRANT_RESTRICTED)) {
  109. return res.status(400).send('the page does not exist');
  110. }
  111. req.body.page = page;
  112. }
  113. catch (error) {
  114. logger.error(error);
  115. return res.status(500).send(`error: ${error}`);
  116. }
  117. return next();
  118. }
  119. // errors.array length is one bacause pageIdRequired is used
  120. const pageIdRequiredError: ValidationError = errors.array()[0];
  121. return res.status(400).send(pageIdRequiredError.msg);
  122. };
  123. return {
  124. renderOgp,
  125. pageIdRequired,
  126. ogpValidator,
  127. };
  128. };