mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-30 17:37:26 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			466 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| /**
 | |
|  * Module dependencies
 | |
|  */
 | |
| 
 | |
| // Node.js core.
 | |
| const crypto = require('crypto');
 | |
| const path = require('path');
 | |
| 
 | |
| // Public node modules.
 | |
| const _ = require('lodash');
 | |
| const fs = require('fs-extra');
 | |
| const io = require('socket.io-client');
 | |
| const request = require('request');
 | |
| const RSA = require('node-rsa');
 | |
| const stringify = require('json-stringify-safe');
 | |
| const unzip = require('unzip2');
 | |
| 
 | |
| /**
 | |
|  * Studio hook
 | |
|  */
 | |
| 
 | |
| module.exports = function (strapi) {
 | |
|   const hook = {
 | |
| 
 | |
|     /**
 | |
|      * Default options
 | |
|      */
 | |
| 
 | |
|     defaults: {
 | |
|       studio: {
 | |
|         enabled: true,
 | |
|         secretKey: ''
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Initialize the hook
 | |
|      */
 | |
| 
 | |
|     initialize: function (cb) {
 | |
|       const _self = this;
 | |
|       let firstConnectionAttempt = true;
 | |
| 
 | |
|       if (_.isPlainObject(strapi.config.studio) && !_.isEmpty(strapi.config.studio) && strapi.config.studio.enabled === true && strapi.config.environment === 'development') {
 | |
|         const manager = io.Manager(strapi.config.studio.url, {
 | |
|           reconnectionDelay: 2000,
 | |
|           reconnectionDelayMax: 5000,
 | |
|           reconnectionAttempts: 5
 | |
|         });
 | |
| 
 | |
|         const socket = io.connect(strapi.config.studio.url, {
 | |
|           'reconnection': true,
 | |
|           'force new connection': true,
 | |
|           'transports': [
 | |
|             'polling',
 | |
|             'websocket',
 | |
|             'htmlfile',
 | |
|             'xhr-polling',
 | |
|             'jsonp-polling'
 | |
|           ]
 | |
|         });
 | |
| 
 | |
|         manager.on('connect_failed', function () {
 | |
|           if (firstConnectionAttempt) {
 | |
|             strapi.log.warn('Connection to the Studio server failed!');
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         manager.on('reconnect_failed', function () {
 | |
|           strapi.log.warn('Reconnection to the Studio server failed!');
 | |
|         });
 | |
| 
 | |
|         manager.on('reconnect', function () {
 | |
|           strapi.log.info('Connection to the Studio server found, please wait a few seconds...');
 | |
|         });
 | |
| 
 | |
|         manager.on('reconnecting', function (number) {
 | |
|           strapi.log.warn('Connection error with the Studio server, new attempt in progress... (' + number + ')');
 | |
|         });
 | |
| 
 | |
|         socket.on('connect', function () {
 | |
|           strapi.log.info('Connection with the Studio server found, please wait a few seconds...');
 | |
|           firstConnectionAttempt = false;
 | |
|           process.nextTick(_self.connectWithStudio(socket));
 | |
|         });
 | |
| 
 | |
|         socket.on('error', function (err) {
 | |
|           strapi.log.warn(err);
 | |
|         });
 | |
| 
 | |
|         socket.on('disconnect', function () {
 | |
|           strapi.log.info('Disconnected from the Studio server.');
 | |
|         });
 | |
| 
 | |
|         socket.on('authorized', function (data) {
 | |
|           const decryptedData = strapi.rsa.decryptPublic(data, 'json');
 | |
| 
 | |
|           if (decryptedData.status === 'ok') {
 | |
|             if (strapi.config.environment === 'development') {
 | |
|               socket.emit('testEncryption', {
 | |
|                 appId: strapi.config.studio.appId,
 | |
|                 token: strapi.token,
 | |
|                 encrypted: strapi.rsa.encrypt({
 | |
|                   secretKey: strapi.config.studio.secretKey,
 | |
|                   data: 'ok'
 | |
|                 })
 | |
|               }, function (err) {
 | |
|                 if (err) {
 | |
|                   strapi.log.warn(err);
 | |
|                 }
 | |
| 
 | |
|                 strapi.log.info('Connected with the Studio server.');
 | |
|               });
 | |
|             } else {
 | |
|               strapi.log.warn('The use of the Studio is restricted to development environment.');
 | |
|             }
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         socket.on('todo', function (data, fn) {
 | |
|           if (!data.hasOwnProperty('from') || !data.hasOwnProperty('to')) {
 | |
|             fn(stringify('Some required attributes are missing', null, 2), null);
 | |
|           } else if (data.from === strapi.token) {
 | |
|             if (data.hasOwnProperty('files')) {
 | |
|               const syncPromise = function (file, index) {
 | |
|                 const deferred = Promise.defer();
 | |
| 
 | |
|                 _self.unzipper(file)
 | |
|                   .then(function () {
 | |
|                     if (!_.isEmpty(data.files[index + 1])) {
 | |
|                       return syncPromise(data.files[index + 1], index + 1);
 | |
|                     } else {
 | |
|                       deferred.resolve();
 | |
|                     }
 | |
|                   })
 | |
|                   .then(function () {
 | |
|                     deferred.resolve();
 | |
|                   })
 | |
|                   .catch(function (err) {
 | |
|                     deferred.reject(err);
 | |
|                   });
 | |
| 
 | |
|                 return deferred.promise;
 | |
|               };
 | |
| 
 | |
|               syncPromise(_.first(data.files), 0)
 | |
|                 .then(function () {
 | |
|                   if (data.hasOwnProperty('action') && _.isFunction(_self[data.action])) {
 | |
|                     _self[data.action](data, function (err, obj) {
 | |
|                       if (err) {
 | |
|                         fn({
 | |
|                           appId: strapi.config.studio.appId,
 | |
|                           token: strapi.token,
 | |
|                           encrypted: strapi.rsa.encrypt({
 | |
|                             err: stringify(err, null, 2),
 | |
|                             data: null
 | |
|                           })
 | |
|                         });
 | |
| 
 | |
|                         return false;
 | |
|                       }
 | |
| 
 | |
|                       fn({
 | |
|                         appId: strapi.config.studio.appId,
 | |
|                         token: strapi.token,
 | |
|                         encrypted: strapi.rsa.encrypt({
 | |
|                           err: null,
 | |
|                           data: stringify(obj, null, 2)
 | |
|                         })
 | |
|                       });
 | |
|                     });
 | |
|                   } else if (!data.hasOwnProperty('action')) {
 | |
|                     fn({
 | |
|                       appId: strapi.config.studio.appId,
 | |
|                       token: strapi.token,
 | |
|                       encrypted: strapi.rsa.encrypt({
 | |
|                         err: null,
 | |
|                         data: stringify(true, null, 2)
 | |
|                       })
 | |
|                     });
 | |
|                   } else {
 | |
|                     fn({
 | |
|                       appId: strapi.config.studio.appId,
 | |
|                       token: strapi.token,
 | |
|                       encrypted: strapi.rsa.encrypt({
 | |
|                         err: stringify('Unknow action', null, 2),
 | |
|                         data: null
 | |
|                       })
 | |
|                     });
 | |
|                   }
 | |
|                 })
 | |
|                 .catch(function (err) {
 | |
|                   fn({
 | |
|                     appId: strapi.config.studio.appId,
 | |
|                     token: strapi.token,
 | |
|                     encrypted: strapi.rsa.encrypt({
 | |
|                       err: err,
 | |
|                       data: null
 | |
|                     })
 | |
|                   });
 | |
|                 });
 | |
|             } else if (!data.hasOwnProperty('action')) {
 | |
|               fn(strapi.rsa.encrypt(stringify('`action` attribute is missing', null, 2)), strapi.rsa.encryptPrivate(null));
 | |
|             } else if (_.isFunction(_self[data.action])) {
 | |
|               _self[data.action](data, function (err, obj) {
 | |
|                 if (err) {
 | |
|                   fn({
 | |
|                     appId: strapi.config.studio.appId,
 | |
|                     token: strapi.token,
 | |
|                     encrypted: strapi.rsa.encrypt({
 | |
|                       err: err,
 | |
|                       data: null
 | |
|                     })
 | |
|                   });
 | |
| 
 | |
|                   return false;
 | |
|                 }
 | |
| 
 | |
|                 fn({
 | |
|                   appId: strapi.config.studio.appId,
 | |
|                   token: strapi.token,
 | |
|                   encrypted: strapi.rsa.encrypt({
 | |
|                     err: null,
 | |
|                     data: stringify(obj, null, 2)
 | |
|                   })
 | |
|                 });
 | |
|               });
 | |
|             } else {
 | |
|               fn({
 | |
|                 appId: strapi.config.studio.appId,
 | |
|                 token: strapi.token,
 | |
|                 encrypted: strapi.rsa.encrypt({
 | |
|                   err: stringify('Unknow action', null, 2),
 | |
|                   data: null
 | |
|                 })
 | |
|               });
 | |
|             }
 | |
|           } else {
 | |
|             fn(stringify('Bad user token', null, 2), null);
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         socket.on('err', function (data) {
 | |
|           strapi.log.warn(data.text);
 | |
|         });
 | |
| 
 | |
|         cb();
 | |
|       } else {
 | |
|         cb();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     connectWithStudio: function (socket) {
 | |
|       strapi.rsa = new RSA({
 | |
|         b: 2048
 | |
|       });
 | |
| 
 | |
|       fs.readFile(path.resolve(process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'], '.strapirc'), {
 | |
|         encoding: 'utf8'
 | |
|       }, function (err, config) {
 | |
|         if (err) {
 | |
|           strapi.log.warn('Continuing without credentials.');
 | |
|         } else {
 | |
|           config = JSON.parse(config);
 | |
|           strapi.token = config.token;
 | |
| 
 | |
|           socket.emit('getPublicKey', null, function (publicKey) {
 | |
|             if (publicKey) {
 | |
|               const key = new RSA();
 | |
|               let object;
 | |
|               key.importKey(publicKey, 'public');
 | |
| 
 | |
|               if (strapi.config.environment === 'development') {
 | |
|                 object = {
 | |
|                   appId: strapi.config.studio.appId,
 | |
|                   appName: strapi.config.name,
 | |
|                   publicKey: strapi.rsa.exportKey('private'),
 | |
|                   secretKey: strapi.config.studio.secretKey,
 | |
|                   dashboardToken: strapi.config.dashboard.token,
 | |
|                   token: strapi.token,
 | |
|                   env: strapi.config.environment
 | |
|                 };
 | |
| 
 | |
|                 socket.emit('check', key.encrypt(object));
 | |
|               }
 | |
|             }
 | |
|           });
 | |
|         }
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Subaction for config
 | |
|      *
 | |
|      * @param {Object} data
 | |
|      *
 | |
|      * @return {Function} cb
 | |
|      */
 | |
| 
 | |
|     handleConfig: function (data, cb) {
 | |
|       strapi.log.warn('We need to flush server.');
 | |
|       strapi.log.warn('Install dependencies if we have to.');
 | |
| 
 | |
|       cb(null, true);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Pull global strapi variable from local server
 | |
|      *
 | |
|      * @param {Object} data
 | |
|      *
 | |
|      * @return {Function} cb
 | |
|      */
 | |
| 
 | |
|     pullServer: function (data, cb) {
 | |
|       const obj = {};
 | |
|       obj.token = strapi.token;
 | |
|       obj.config = strapi.config;
 | |
|       obj.models = strapi.models;
 | |
|       obj.api = strapi.api;
 | |
|       obj.templates = {};
 | |
| 
 | |
|       cb(null, obj);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Rebuild dictionary
 | |
|      *
 | |
|      * @param {Object} data
 | |
|      *
 | |
|      * @return {Function} cb
 | |
|      */
 | |
| 
 | |
|     rebuild: function (data, cb) {
 | |
|       strapi.rebuild();
 | |
| 
 | |
|       cb(null, true);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Remove file or folder
 | |
|      *
 | |
|      * @param {Object} data
 | |
|      *
 | |
|      * @return {Function} cb
 | |
|      */
 | |
| 
 | |
|     removeFileOrFolder: function (data, cb) {
 | |
|       const arrayOfPromises = [];
 | |
| 
 | |
|       if (data.hasOwnProperty('toRemove') && _.isArray(data.toRemove)) {
 | |
|         const toRemove = function (path) {
 | |
|           const deferred = Promise.defer();
 | |
| 
 | |
|           fs.exists(path, function (exists) {
 | |
|             if (exists) {
 | |
|               fs.remove(path, function (err) {
 | |
|                 if (err) {
 | |
|                   deferred.reject(err);
 | |
|                 } else {
 | |
|                   deferred.resolve();
 | |
|                 }
 | |
|               });
 | |
|             } else {
 | |
|               deferred.reject('Unknow path `' + path + '`');
 | |
|             }
 | |
|           });
 | |
| 
 | |
|           return deferred.promise;
 | |
|         };
 | |
| 
 | |
|         _.forEach(data.toRemove, function (fileOrFolder) {
 | |
|           arrayOfPromises.push(toRemove(fileOrFolder.path));
 | |
|         });
 | |
| 
 | |
|         Promise.all(arrayOfPromises)
 | |
|           .then(function () {
 | |
|             cb(null, true);
 | |
|           })
 | |
|           .catch(function (err) {
 | |
|             cb(err, null);
 | |
|           });
 | |
|       } else {
 | |
|         cb('Attribute `toRemove` is missing or is not an array', null);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Function to unzip file or folder to specific folder
 | |
|      *
 | |
|      * @param {Object} data
 | |
|      *
 | |
|      * @return {Function} cb
 | |
|      */
 | |
|     unzipper: function (data) {
 | |
|       const deferred = Promise.defer();
 | |
| 
 | |
|       crypto.pbkdf2(data.token, strapi.token, 4096, 32, 'sha256', function (err, derivedKey) {
 | |
|         if (err) {
 | |
|           deferred.reject(err);
 | |
|         }
 | |
| 
 | |
|         const fileToUnzip = path.resolve(strapi.config.appPath, '.tmp', derivedKey.toString('hex') + '.zip');
 | |
| 
 | |
|         request({
 | |
|           method: 'POST',
 | |
|           preambleCRLF: true,
 | |
|           postambleCRLF: true,
 | |
|           json: true,
 | |
|           uri: strapi.config.studio.url + '/socket/download',
 | |
|           encoding: null,
 | |
|           body: {
 | |
|             token: strapi.token,
 | |
|             fileId: data.token,
 | |
|             src: data.src
 | |
|           }
 | |
|         })
 | |
|         .pipe(fs.createWriteStream(fileToUnzip))
 | |
|         .on('close', function () {
 | |
| 
 | |
|           let folderDest;
 | |
|           const folderOrFiletoRemove = path.resolve(data.dest);
 | |
| 
 | |
|           if (data.src === 'api') {
 | |
|             folderDest = folderOrFiletoRemove;
 | |
|           } else {
 | |
|             folderDest = path.resolve(data.dest, '..');
 | |
|           }
 | |
| 
 | |
|           fs.remove(folderOrFiletoRemove, function (err) {
 | |
|             if (err) {
 | |
|               deferred.reject(err);
 | |
|             }
 | |
| 
 | |
|             _.defer(function () {
 | |
|               fs.createReadStream(fileToUnzip).pipe(unzip.Extract({
 | |
|                 path: folderDest
 | |
|               }))
 | |
|                 .on('close', function () {
 | |
|                   fs.remove(fileToUnzip, function (err) {
 | |
|                     if (err) {
 | |
|                       deferred.reject(err);
 | |
|                     }
 | |
| 
 | |
|                     deferred.resolve();
 | |
|                   });
 | |
|                 }).on('error', function (err) {
 | |
|                   deferred.reject(err);
 | |
|                 });
 | |
|             });
 | |
|           });
 | |
|         })
 | |
|         .on('error', function () {
 | |
|           deferred.reject('Download ZIP or unzip not worked fine', null);
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       return deferred.promise;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return hook;
 | |
| };
 | 
