plugin.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import fs from 'fs';
  2. import path from 'path';
  3. import mongoose from 'mongoose';
  4. import wget from 'node-wget-js';
  5. import streamToPromise from 'stream-to-promise';
  6. import unzipper from 'unzipper';
  7. import { GrowiPlugin, GrowiPluginMeta, GrowiPluginOrigin } from '~/interfaces/plugin';
  8. import loggerFactory from '~/utils/logger';
  9. import { resolveFromRoot } from '~/utils/project-dir-utils';
  10. // eslint-disable-next-line import/no-cycle
  11. import Crowi from '../crowi';
  12. const logger = loggerFactory('growi:plugins:plugin-utils');
  13. const pluginStoringPath = resolveFromRoot('tmp/plugins');
  14. export class PluginService {
  15. crowi: any;
  16. growiBridgeService: any;
  17. baseDir: any;
  18. getFile:any;
  19. constructor(crowi) {
  20. this.crowi = crowi;
  21. this.growiBridgeService = crowi.growiBridgeService;
  22. this.baseDir = path.join(crowi.tmpDir, 'plugins');
  23. this.getFile = this.growiBridgeService.getFile.bind(this);
  24. }
  25. async install(crowi: Crowi, origin: GrowiPluginOrigin): Promise<void> {
  26. // download
  27. // const ghUrl = origin.url;
  28. // const downloadDir = path.join(process.cwd(), 'tmp/plugins/');
  29. // try {
  30. // await this.downloadZipFile(`${ghUrl}/archive/refs/heads/master.zip`, downloadDir);
  31. // }
  32. // catch (err) {
  33. // // TODO: error handling
  34. // }
  35. // TODO: detect plugins
  36. // TODO: save documents
  37. const installedSamplePath = '/workspace/growi/packages/app/tmp/plugins/hogerepository/meta.json';
  38. await this.savePluginMetaData(installedSamplePath);
  39. return;
  40. }
  41. async savePluginMetaData(installedPath: string): Promise<void> {
  42. const metaData = this.getPluginMetaData(installedPath);
  43. const GrowiPlugin = mongoose.model('GrowiPlugin');
  44. // await GrowiPlugin.insertMany({
  45. // isEnabled: true,
  46. // installedPath,
  47. // meta: {
  48. // name: metaData.name,
  49. // types: metaData.types,
  50. // author: metaData.author,
  51. // },
  52. // });
  53. }
  54. private getPluginMetaData(installedPath: string): GrowiPluginMeta {
  55. const metaDataJSON = JSON.parse(fs.readFileSync(installedPath, 'utf-8'));
  56. return metaDataJSON;
  57. }
  58. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  59. static async detectPlugins(origin: GrowiPluginOrigin, installedPath: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
  60. const packageJsonPath = path.resolve(pluginStoringPath, installedPath, 'package.json');
  61. const packageJson = await import(packageJsonPath);
  62. const { growiPlugin } = packageJson;
  63. const {
  64. name: packageName, description: packageDesc, author: packageAuthor,
  65. } = parentPackageJson ?? packageJson;
  66. if (growiPlugin == null) {
  67. throw new Error('This package does not include \'growiPlugin\' section.');
  68. }
  69. // detect sub plugins for monorepo
  70. if (growiPlugin.isMonorepo && growiPlugin.packages != null) {
  71. const plugins = await Promise.all(
  72. growiPlugin.packages.map(async(subPackagePath) => {
  73. const subPackageInstalledPath = path.join(installedPath, subPackagePath);
  74. return this.detectPlugins(origin, subPackageInstalledPath, packageJson);
  75. }),
  76. );
  77. return plugins.flat();
  78. }
  79. if (growiPlugin.types == null) {
  80. throw new Error('\'growiPlugin\' section must have a \'types\' property.');
  81. }
  82. const plugin = {
  83. isEnabled: true,
  84. installedPath,
  85. origin,
  86. meta: {
  87. name: growiPlugin.name ?? packageName,
  88. desc: growiPlugin.desc ?? packageDesc,
  89. author: growiPlugin.author ?? packageAuthor,
  90. types: growiPlugin.types,
  91. },
  92. };
  93. logger.info('Plugin detected => ', plugin);
  94. return [plugin];
  95. }
  96. async listPlugins(): Promise<GrowiPlugin[]> {
  97. return [];
  98. }
  99. async downloadZipFile(ghUrl: string, filename:string): Promise<void> {
  100. wget({ url: ghUrl, dest: filename });
  101. try {
  102. const zipFile = await this.getFile('master.zip');
  103. await this.unzip(zipFile);
  104. }
  105. catch (err) {
  106. // console.log('fail');
  107. }
  108. return;
  109. }
  110. /**
  111. * extract a zip file
  112. *
  113. * @memberOf ImportService
  114. * @param {string} zipFile absolute path to zip file
  115. * @return {Array.<string>} array of absolute paths to extracted files
  116. */
  117. async unzip(zipFile) {
  118. // const stream = fs.createReadStream(zipFile).pipe(unzipper.Extract({ path: '/workspace/growi/packages/app/tmp/plugins' }));
  119. // try {
  120. // await streamToPromise(stream);
  121. // }
  122. // catch (err) {
  123. // console.log('err', err);
  124. // }
  125. const readStream = fs.createReadStream(zipFile);
  126. const unzipStream = readStream.pipe(unzipper.parse());
  127. const files: any = [];
  128. unzipStream.on('entry', async(entry) => {
  129. const fileName = entry.path;
  130. // https://regex101.com/r/mD4eZs/6
  131. // prevent from unexpecting attack doing unzip file (path traversal attack)
  132. // FOR EXAMPLE
  133. // ../../src/server/views/admin/markdown.html
  134. if (fileName.match(/(\.\.\/|\.\.\\)/)) {
  135. logger.error('File path is not appropriate.', fileName);
  136. return;
  137. }
  138. if (fileName === this.growiBridgeService.getMetaFileName()) {
  139. // skip meta.json
  140. entry.autodrain();
  141. }
  142. else {
  143. const jsonFile = path.join(this.baseDir, fileName);
  144. const writeStream = fs.createWriteStream(jsonFile, { encoding: this.growiBridgeService.getEncoding() });
  145. entry.pipe(writeStream);
  146. files.push(jsonFile);
  147. }
  148. });
  149. await streamToPromise(unzipStream);
  150. return files;
  151. }
  152. }