diff --git a/.eslintignore b/.eslintignore index 99076049..09225beb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ build/*.js config/*.js src/libs/*.js +./index.js diff --git a/build/dev-server.js b/build/dev-server.js index a367ff2e..fbae2240 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -24,7 +24,7 @@ var app = express() var compiler = webpack(webpackConfig) // StackEdit custom middlewares -require('./server')(app); +require('../server')(app); var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, @@ -62,8 +62,8 @@ app.use(devMiddleware) app.use(hotMiddleware) // serve pure static assets -// var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) -app.use(express.static('./static')) +var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) +app.use(staticPath, express.static('./static')) var uri = 'http://localhost:' + port diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index b048df9b..bb70a031 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -55,7 +55,10 @@ module.exports = { } }, { - test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader' + }, + { + test: /\.woff(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js index da44b656..0c88ed89 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -8,6 +8,7 @@ var CopyWebpackPlugin = require('copy-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin') var ExtractTextPlugin = require('extract-text-webpack-plugin') var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') +var OfflinePlugin = require('offline-plugin'); var env = config.build.env @@ -90,7 +91,11 @@ var webpackConfig = merge(baseWebpackConfig, { to: config.build.assetsSubDirectory, ignore: ['.*'] } - ]) + ]), + new OfflinePlugin({ + excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html'], + externals: ['/app', '/oauth2/callback'] + }) ] }) diff --git a/index.js b/index.js new file mode 100644 index 00000000..bfa2eaad --- /dev/null +++ b/index.js @@ -0,0 +1,27 @@ +var cluster = require('cluster'); +var http = require('http'); +var https = require('https'); +var path = require('path'); +var express = require('express'); +var app = express(); + +require('./server')(app); + +var port = process.env.PORT || 8080; +if(port === 443) { + var fs = require('fs'); + var credentials = { + key: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.key'), 'utf8'), + cert: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.crt'), 'utf8'), + ca: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.ca'), 'utf8').split('\n\n') + }; + var httpsServer = https.createServer(credentials, app); + httpsServer.listen(port, null, function() { + console.log('HTTPS server started: https://localhost'); + }); + port = 80; +} +var httpServer = http.createServer(app); +httpServer.listen(port, null, function() { + console.log('HTTP server started: http://localhost:' + port); +}); diff --git a/package.json b/package.json index 06019093..0a3ef000 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { - "name": "StackEdit", - "version": "1.0.0", - "description": "A Vue.js project", - "author": "", - "private": true, + "name": "stackedit", + "version": "5.0.0", + "description": "Free, open-source, full-featured Markdown editor", + "author": "Benoit Schweblin", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/benweet/stackedit/issues" + }, + "main": "index.js", "scripts": { "postinstall": "gulp --cwd build build-prism", "start": "node build/dev-server.js", @@ -14,6 +18,7 @@ "bezier-easing": "^1.1.0", "clipboard": "^1.7.1", "clunderscore": "^1.0.3", + "compression": "^1.7.0", "diff-match-patch": "^1.0.0", "file-saver": "^1.3.3", "handlebars": "^4.0.10", @@ -33,6 +38,8 @@ "prismjs": "^1.6.0", "raw-loader": "^0.5.1", "request": "^2.82.0", + "serve-static": "^1.12.6", + "stackedit": "^4.3.15", "vue": "^2.3.3", "vuex": "^2.3.1" }, @@ -58,7 +65,7 @@ "eslint-plugin-html": "^2.0.0", "eslint-plugin-import": "^2.2.0", "eventsource-polyfill": "^0.9.6", - "express": "^4.14.1", + "express": "^4.15.5", "extract-text-webpack-plugin": "^2.0.0", "file-loader": "^0.11.1", "friendly-errors-webpack-plugin": "^1.1.3", @@ -66,7 +73,9 @@ "gulp-concat": "^2.6.1", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", + "ignore-loader": "^0.1.2", "node-sass": "^4.5.3", + "offline-plugin": "^4.8.4", "opn": "^4.0.2", "optimize-css-assets-webpack-plugin": "^1.3.0", "ora": "^1.2.0", diff --git a/build/server.js b/server/github.js similarity index 63% rename from build/server.js rename to server/github.js index 75ebf03e..d2335fbb 100644 --- a/build/server.js +++ b/server/github.js @@ -15,7 +15,6 @@ function githubToken(clientId, code) { if (err) { reject(err); } - console.log(body) var token = qs.parse(body).access_token; if (token) { resolve(token); @@ -26,13 +25,11 @@ function githubToken(clientId, code) { }); } -module.exports = function (app) { - app.get('/oauth2/githubToken', function (req, res) { - githubToken(req.query.clientId, req.query.code) - .then(function (token) { - res.send(token); - }, function (err) { - res.status(400).send(err ? err.message || err.toString() : 'bad_code'); - }); - }); +exports.githubToken = function (req, res) { + githubToken(req.query.clientId, req.query.code) + .then(function (token) { + res.send(token); + }, function (err) { + res.status(400).send(err ? err.message || err.toString() : 'bad_code'); + }); }; diff --git a/server/index.js b/server/index.js new file mode 100644 index 00000000..fcf40c65 --- /dev/null +++ b/server/index.js @@ -0,0 +1,50 @@ +var compression = require('compression'); +var serveStatic = require('serve-static'); +var path = require('path'); + +module.exports = function (app) { + // Force HTTPS on stackedit.io + app.all('*', function(req, res, next) { + if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') { + return res.redirect('https://stackedit.io' + req.url); + } + /\.(eot|ttf|woff|svg)$/.test(req.url) && res.header('Access-Control-Allow-Origin', '*'); + next(); + }); + + // Use gzip compression + app.use(compression()); + + app.post('/pdfExport', require('./pdf').export); + app.get('/oauth2/githubToken', require('./github').githubToken); + + // Serve landing.html in / + app.get('/', function(req, res) { + res.sendFile(require.resolve('stackedit/views/landing.html')); + }); + // Serve editor.html in /viewer + app.get('/editor', function(req, res) { + res.sendFile(require.resolve('stackedit/views/editor.html')); + }); + // Serve viewer.html in /viewer + app.get('/viewer', function(req, res) { + res.sendFile(require.resolve('stackedit/views/viewer.html')); + }); + // Serve index.html in /app + app.get('/app', function(req, res) { + res.sendFile(path.join(__dirname, '../dist/index.html')); + }); + // Serve callback.html in /app + app.get('/oauth2/callback', function(req, res) { + res.sendFile(path.join(__dirname, '../dist/static/oauth2/callback.html')); + }); + + // Serve static resources + app.use(serveStatic(path.join(__dirname, '../dist'))); // v5 + app.use(serveStatic(path.dirname(require.resolve('stackedit/public/cache.manifest')))); // v4 + + // Error 404 + app.use(function(req, res) { + res.status(404).sendFile(require.resolve('stackedit/views/error_404.html')); + }); +}; diff --git a/server/pdf.js b/server/pdf.js new file mode 100644 index 00000000..80bd61e4 --- /dev/null +++ b/server/pdf.js @@ -0,0 +1,145 @@ +/* global window,MathJax */ +var spawn = require('child_process').spawn; +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var request = require('request'); + +function waitForJavaScript() { + if(window.MathJax) { + // Amazon EC2: fix TeX font detection + MathJax.Hub.Register.StartupHook("HTML-CSS Jax Startup",function () { + var HTMLCSS = MathJax.OutputJax["HTML-CSS"]; + HTMLCSS.Font.checkWebFont = function (check,font,callback) { + if (check.time(callback)) { + return; + } + if (check.total === 0) { + HTMLCSS.Font.testFont(font); + setTimeout(check,200); + } else { + callback(check.STATUS.OK); + } + }; + }); + MathJax.Hub.Queue(function () { + window.status = 'done'; + }); + } + else { + setTimeout(function() { + window.status = 'done'; + }, 2000); + } +} + +var authorizedPageSizes = [ + 'A3', + 'A4', + 'Legal', + 'Letter' +]; + +exports.export = function(req, res, next) { + function onError(err) { + next(err); + } + function onUnknownError() { + res.statusCode = 400; + res.end('Unknown error'); + } + function onUnauthorizedError() { + res.statusCode = 401; + res.end('Unauthorized'); + } + function onTimeout() { + res.statusCode = 408; + res.end('Request timeout'); + } + request({ + uri: 'https://monetizejs.com/api/payments', + qs: { + access_token: req.query.token + }, + json: true + }, function (err, paymentsRes, payments) { + var authorized = payments && payments.app == 'ESTHdCYOi18iLhhO' && ( + (payments.chargeOption && payments.chargeOption.alias == 'once') || + (payments.subscriptionOption && payments.subscriptionOption.alias == 'yearly')); + if(err || paymentsRes.statusCode != 200 || !authorized) { + return onUnauthorizedError(); + } + var options, params = []; + try { + options = JSON.parse(req.query.options); + } + catch(e) { + options = {}; + } + + // Margins + var marginTop = parseInt(options.marginTop); + params.push('-T', isNaN(marginTop) ? 25 : marginTop); + var marginRight = parseInt(options.marginRight); + params.push('-R', isNaN(marginRight) ? 25 : marginRight); + var marginBottom = parseInt(options.marginBottom); + params.push('-B', isNaN(marginBottom) ? 25 : marginBottom); + var marginLeft = parseInt(options.marginLeft); + params.push('-L', isNaN(marginLeft) ? 25 : marginLeft); + + // Header + options.headerCenter && params.push('--header-center', options.headerCenter); + options.headerLeft && params.push('--header-left', options.headerLeft); + options.headerRight && params.push('--header-right', options.headerRight); + options.headerFontName && params.push('--header-font-name', options.headerFontName); + options.headerFontSize && params.push('--header-font-size', options.headerFontSize); + + // Footer + options.footerCenter && params.push('--footer-center', options.footerCenter); + options.footerLeft && params.push('--footer-left', options.footerLeft); + options.footerRight && params.push('--footer-right', options.footerRight); + options.footerFontName && params.push('--footer-font-name', options.footerFontName); + options.footerFontSize && params.push('--footer-font-size', options.footerFontSize); + + // Page size + params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize); + + // Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason + var filePath = path.join(os.tmpDir(), Date.now() + '.pdf'); + var binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf'; + params.push('--run-script', waitForJavaScript.toString() + 'waitForJavaScript()'); + params.push('--window-status', 'done'); + var wkhtmltopdf = spawn(binPath, params.concat('-', filePath), { + stdio: [ + 'pipe', + 'ignore', + 'ignore' + ] + }); + var timeoutId = setTimeout(function() { + timeoutId = undefined; + wkhtmltopdf.kill(); + }, 30000); + wkhtmltopdf.on('error', onError); + wkhtmltopdf.stdin.on('error', onError); + wkhtmltopdf.on('close', function(code) { + if(!timeoutId) { + return onTimeout(); + } + clearTimeout(timeoutId); + if(code) { + return onUnknownError(); + } + var readStream = fs.createReadStream(filePath); + readStream.on('open', function() { + readStream.pipe(res); + }); + readStream.on('close', function() { + fs.unlink(filePath, function() { + }); + }); + readStream.on('error', onUnknownError); + }); + req.pipe(wkhtmltopdf.stdin); + }); +}; diff --git a/src/assets/iconWordpress.svg b/src/assets/iconWordpress.svg new file mode 100644 index 00000000..1c24b9b7 --- /dev/null +++ b/src/assets/iconWordpress.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/assets/iconZendesk.svg b/src/assets/iconZendesk.svg new file mode 100644 index 00000000..7eec78a5 --- /dev/null +++ b/src/assets/iconZendesk.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/components/ExplorerNode.vue b/src/components/ExplorerNode.vue index 881223c8..eb6de6fb 100644 --- a/src/components/ExplorerNode.vue +++ b/src/components/ExplorerNode.vue @@ -20,7 +20,6 @@ diff --git a/src/components/menus/SyncMenu.vue b/src/components/menus/SyncMenu.vue index ce1cf756..b526d89f 100644 --- a/src/components/menus/SyncMenu.vue +++ b/src/components/menus/SyncMenu.vue @@ -81,7 +81,6 @@ import dropboxHelper from '../../services/providers/helpers/dropboxHelper'; import githubHelper from '../../services/providers/helpers/githubHelper'; import googleDriveProvider from '../../services/providers/googleDriveProvider'; import dropboxProvider from '../../services/providers/dropboxProvider'; -import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider'; import syncSvc from '../../services/syncSvc'; import store from '../../store'; @@ -142,10 +141,16 @@ export default { return googleHelper.addDriveAccount(); }, addDropboxAccount() { - return dropboxHelper.addAccount(); + return this.$store.dispatch('modal/open', { + type: 'dropboxAccount', + onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess), + }); }, addGithubAccount() { - return githubHelper.addAccount(); + return this.$store.dispatch('modal/open', { + type: 'githubAccount', + onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess), + }); }, openGoogleDrive(token) { return googleHelper.openPicker(token, 'doc') @@ -155,12 +160,7 @@ export default { openDropbox(token) { return dropboxHelper.openChooser(token) .then(paths => this.$store.dispatch('queue/enqueue', - () => { - if (token.fullAccess) { - return dropboxProvider.openFiles(token, paths); - } - return dropboxRestrictedProvider.openFiles(token, paths); - })); + () => dropboxProvider.openFiles(token, paths))); }, saveGoogleDrive(token) { return openSyncModal(token, 'googleDriveSync'); diff --git a/src/components/modals/BloggerPagePublishModal.vue b/src/components/modals/BloggerPagePublishModal.vue index 2677dd62..b85f6a6c 100644 --- a/src/components/modals/BloggerPagePublishModal.vue +++ b/src/components/modals/BloggerPagePublishModal.vue @@ -1,40 +1,31 @@ diff --git a/src/components/modals/BloggerPublishModal.vue b/src/components/modals/BloggerPublishModal.vue index 728b0802..eac2b942 100644 --- a/src/components/modals/BloggerPublishModal.vue +++ b/src/components/modals/BloggerPublishModal.vue @@ -1,41 +1,32 @@ diff --git a/src/components/modals/DropboxAccountModal.vue b/src/components/modals/DropboxAccountModal.vue new file mode 100644 index 00000000..2b5cc560 --- /dev/null +++ b/src/components/modals/DropboxAccountModal.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/modals/DropboxPublishModal.vue b/src/components/modals/DropboxPublishModal.vue index 0a17aa14..00401a5f 100644 --- a/src/components/modals/DropboxPublishModal.vue +++ b/src/components/modals/DropboxPublishModal.vue @@ -1,33 +1,27 @@ diff --git a/src/components/modals/DropboxSyncModal.vue b/src/components/modals/DropboxSyncModal.vue index 013482ea..27d6085c 100644 --- a/src/components/modals/DropboxSyncModal.vue +++ b/src/components/modals/DropboxSyncModal.vue @@ -1,20 +1,17 @@ diff --git a/src/components/modals/FilePropertiesModal.vue b/src/components/modals/FilePropertiesModal.vue index 552990b8..c79ff0e6 100644 --- a/src/components/modals/FilePropertiesModal.vue +++ b/src/components/modals/FilePropertiesModal.vue @@ -19,7 +19,7 @@ @@ -38,6 +38,7 @@ export default { CodeEditor, }, data: () => ({ + contentId: null, tab: 'custom', defaultProperties, customProperties: null, @@ -52,7 +53,9 @@ export default { }, }, created() { - const properties = this.$store.getters['content/current'].properties; + const content = this.$store.getters['content/current']; + this.contentId = content.id; + const properties = content.properties; this.setCustomProperties(properties === '\n' ? emptyProperties : properties); }, methods: { @@ -65,6 +68,15 @@ export default { this.error = e.message; } }, + resolve() { + if (!this.error) { + this.$store.commit('content/patchItem', { + id: this.contentId, + properties: this.strippedCustomProperties, + }); + this.config.resolve(); + } + }, }, }; diff --git a/src/components/modals/FormEntry.vue b/src/components/modals/FormEntry.vue new file mode 100644 index 00000000..390e34ed --- /dev/null +++ b/src/components/modals/FormEntry.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/modals/GistPublishModal.vue b/src/components/modals/GistPublishModal.vue index f524cba1..87fdc4ee 100644 --- a/src/components/modals/GistPublishModal.vue +++ b/src/components/modals/GistPublishModal.vue @@ -1,16 +1,13 @@ diff --git a/src/components/modals/GistSyncModal.vue b/src/components/modals/GistSyncModal.vue index 8e31892b..4ba24702 100644 --- a/src/components/modals/GistSyncModal.vue +++ b/src/components/modals/GistSyncModal.vue @@ -1,16 +1,13 @@ diff --git a/src/components/modals/GithubAccountModal.vue b/src/components/modals/GithubAccountModal.vue new file mode 100644 index 00000000..b81087fa --- /dev/null +++ b/src/components/modals/GithubAccountModal.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/modals/GithubPublishModal.vue b/src/components/modals/GithubPublishModal.vue index dc91e1dc..865bfc53 100644 --- a/src/components/modals/GithubPublishModal.vue +++ b/src/components/modals/GithubPublishModal.vue @@ -1,51 +1,39 @@ diff --git a/src/components/modals/GithubSyncModal.vue b/src/components/modals/GithubSyncModal.vue index d19c9edf..7e691bd5 100644 --- a/src/components/modals/GithubSyncModal.vue +++ b/src/components/modals/GithubSyncModal.vue @@ -1,38 +1,29 @@ diff --git a/src/components/modals/GoogleDrivePublishModal.vue b/src/components/modals/GoogleDrivePublishModal.vue index 7c937fd4..8a2a44b9 100644 --- a/src/components/modals/GoogleDrivePublishModal.vue +++ b/src/components/modals/GoogleDrivePublishModal.vue @@ -1,19 +1,25 @@ diff --git a/src/components/modals/GoogleDriveSyncModal.vue b/src/components/modals/GoogleDriveSyncModal.vue index 59e9597c..3ae07ddd 100644 --- a/src/components/modals/GoogleDriveSyncModal.vue +++ b/src/components/modals/GoogleDriveSyncModal.vue @@ -1,34 +1,25 @@ diff --git a/src/components/modals/GooglePhotoModal.vue b/src/components/modals/GooglePhotoModal.vue index 4b15efd2..03d24984 100644 --- a/src/components/modals/GooglePhotoModal.vue +++ b/src/components/modals/GooglePhotoModal.vue @@ -2,18 +2,12 @@