diff options
41 files changed, 278 insertions, 580 deletions
@@ -8,6 +8,7 @@ HackMD Community Edition [![build status][travis-image]][travis-url] [![version][github-version-badge]][github-release-page] [![Help Contribute to Open Source][codetriage-image]][codetriage-url] +[![POEditor][poeditor-image]][poeditor-url] HackMD lets you create realtime collaborative markdown notes on all platforms. Inspired by Hackpad, with more focus on speed and flexibility. @@ -114,12 +115,19 @@ If you are upgrading HackMD from an older version, follow these steps: 6. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema 7. Start your whole new server! -* [migration-to-0.5.0](https://github.com/hackmdio/migration-to-0.5.0) + +* **migrate-to-1.1.0** + +We deprecated the older lower case config style and moved on to camel case style. Please have a look at the current `config.json.example` and check the warnings on startup. + +*Notice: This is not a breaking change right now but in the future* + +* [**migration-to-0.5.0**](https://github.com/hackmdio/migration-to-0.5.0) We don't use LZString to compress socket.io data and DB data after version 0.5.0. Please run the migration tool if you're upgrading from the old version. -* [migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0) +* [**migration-to-0.4.0**](https://github.com/hackmdio/migration-to-0.4.0) We've dropped MongoDB after version 0.4.0. So here is the migration tool for you to transfer the old DB data to the new DB. @@ -151,6 +159,8 @@ There are some config settings you need to change in the files below. | `HMD_ALLOW_FREEURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | | `HMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) | | `HMD_DB_URL` | `mysql://localhost:3306/database` | set the database URL | +| `HMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If non is set, one will randomly generated on startup | +| `HMD_SESSION_LIFE` | `1209600000` | Session life time. (milliseconds) | | `HMD_FACEBOOK_CLIENTID` | no example | Facebook API client id | | `HMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret | | `HMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key | @@ -262,7 +272,7 @@ There are some config settings you need to change in the files below. | ------- | --------- | ----------- | | facebook, twitter, github, gitlab, mattermost, dropbox, google, ldap, saml | environment variables or `config.json` | for signin | | imgur, s3, minio | environment variables or `config.json` | for image upload | -| google drive(`google/apiKey`, `google/clientID`), dropbox(`dropbox/appKey`) | `config.json` | for export and import | +| dropbox(`dropbox/appKey`) | `config.json` | for export and import | ## Third-party integration OAuth callback URLs @@ -318,3 +328,5 @@ See more at [http://operational-transformation.github.io/](http://operational-tr [standardjs-url]: https://github.com/feross/standard [codetriage-image]: https://www.codetriage.com/hackmdio/hackmd/badges/users.svg [codetriage-url]: https://www.codetriage.com/hackmdio/hackmd +[poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg +[poeditor-url]: https://poeditor.com/join/project/1OpGjF2Jir @@ -33,8 +33,6 @@ var data = { urlpath: config.urlPath, debug: config.debug, version: config.version, - GOOGLE_API_KEY: config.google.clientSecret, - GOOGLE_CLIENT_ID: config.google.clientID, DROPBOX_APP_KEY: config.dropbox.appKey, allowedUploadMimeTypes: config.allowedUploadMimeTypes } @@ -23,6 +23,10 @@ "description": "Specify database type. See sequelize available databases. Default using postgres", "value": "postgres" }, + "HMD_SESSION_SECRET": { + "description": "Secret used to secure session cookies.", + "required": false + }, "HMD_HSTS_ENABLE": { "description": "whether to also use HSTS if HTTPS is enabled", "required": false @@ -132,10 +136,6 @@ "description": "Google API client secret", "required": false }, - "HMD_GOOGLE_API_KEY": { - "description": "Google API key (for import/export)", - "required": false - }, "HMD_IMGUR_CLIENTID": { "description": "Imgur API client id", "required": false diff --git a/config.json.example b/config.json.example index 8d1b6abd..6cd55efb 100644 --- a/config.json.example +++ b/config.json.example @@ -22,12 +22,14 @@ "includeSubdomains": true, "preload": true }, - csp: { + "csp": { "enable": true, "directives": { }, - "upgradeInsecureRequests": "auto" - "addDefaults": true + "upgradeInsecureRequests": "auto", + "addDefaults": true, + "addDisqus": true, + "addGoogleAnalytics": true }, "db": { "username": "", diff --git a/docs/guides/auth.md b/docs/guides/auth.md index aa629489..e4261724 100644 --- a/docs/guides/auth.md +++ b/docs/guides/auth.md @@ -210,3 +210,32 @@ The basic procedure is the same as the case of OneLogin which is mentioned above ```` +### GitLab (self-hosted) + +1. Sign in to your GitLab +2. Navigate to the application management page at `https://your.gitlab.domain/admin/applications` (admin permissions required) +3. Click **New application** to create a new application and fill out the registration form: + +![New GitLab application](images/auth/gitlab-new-application.png) + +4. Click **Submit** +5. In the list of applications select **HackMD**. Leave that site open to copy the application ID and secret in the next step. + +![Application: HackMD](images/auth/gitlab-application-details.png) + + +6. In the `docker-compose.yml` add the following environment variables to `app:` `environment:` + +``` +- HMD_DOMAIN=your.hackmd.domain +- HMD_URL_ADDPORT=443 +- HMD_PROTOCOL_USESSL=true +- HMD_GITLAB_BASEURL=https://your.gitlab.domain +- HMD_GITLAB_CLIENTID=23462a34example99fid0943c3fde97310fb7db47fab1112 +- HMD_GITLAB_CLIENTSECRET=5532e9dexample70432secret0c37dd20ce077e6073ea9f1d6 +``` + +7. Run `docker-compose up -d` to apply your settings. +8. Sign in to your HackMD using your GitLab ID: + +![Sign in via GitLab](images/auth/gitlab-sign-in.png) diff --git a/docs/guides/images/auth/gitlab-application-details.png b/docs/guides/images/auth/gitlab-application-details.png Binary files differnew file mode 100644 index 00000000..6e042886 --- /dev/null +++ b/docs/guides/images/auth/gitlab-application-details.png diff --git a/docs/guides/images/auth/gitlab-new-application.png b/docs/guides/images/auth/gitlab-new-application.png Binary files differnew file mode 100644 index 00000000..be9e4446 --- /dev/null +++ b/docs/guides/images/auth/gitlab-new-application.png diff --git a/docs/guides/images/auth/gitlab-sign-in.png b/docs/guides/images/auth/gitlab-sign-in.png Binary files differnew file mode 100644 index 00000000..27aaf6dd --- /dev/null +++ b/docs/guides/images/auth/gitlab-sign-in.png diff --git a/lib/config/default.js b/lib/config/default.js index 19ddccf6..68849d36 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -18,6 +18,8 @@ module.exports = { directives: { }, addDefaults: true, + addDisqus: true, + addGoogleAnalytics: true, upgradeInsecureRequests: 'auto', reportURI: undefined }, @@ -46,6 +48,7 @@ module.exports = { // session sessionName: 'connect.sid', sessionSecret: 'secret', + sessionSecretLen: 128, sessionLife: 14 * 24 * 60 * 60 * 1000, // 14 days staticCacheTime: 1 * 24 * 60 * 60 * 1000, // 1 day // socket.io diff --git a/lib/config/defaultSSL.js b/lib/config/defaultSSL.js index 362c62a1..ba020466 100644 --- a/lib/config/defaultSSL.js +++ b/lib/config/defaultSSL.js @@ -10,8 +10,8 @@ function getFile (path) { } module.exports = { - sslkeypath: getFile('/run/secrets/key.pem'), - sslcertpath: getFile('/run/secrets/cert.pem'), - sslcapath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], - dhparampath: getFile('/run/secrets/dhparam.pem') + sslKeyPath: getFile('/run/secrets/key.pem'), + sslCertPath: getFile('/run/secrets/cert.pem'), + sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], + dhParamPath: getFile('/run/secrets/dhparam.pem') } diff --git a/lib/config/environment.js b/lib/config/environment.js index cab3bc3e..3dde4786 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -26,6 +26,8 @@ module.exports = { allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), defaultPermission: process.env.HMD_DEFAULT_PERMISSION, dbURL: process.env.HMD_DB_URL, + sessionSecret: process.env.HMD_SESSION_SECRET, + sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE), imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE, imgur: { clientID: process.env.HMD_IMGUR_CLIENTID diff --git a/lib/config/index.js b/lib/config/index.js index fae51e52..bdba5e0e 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,6 +1,7 @@ 'use strict' +const crypto = require('crypto') const fs = require('fs') const path = require('path') const {merge} = require('lodash') @@ -52,7 +53,7 @@ if (config.ldap.tlsca) { // Permission config.permission = Permission -if (!config.allowAnonymous && !config.allowAnonymousedits) { +if (!config.allowAnonymous && !config.allowAnonymousEdits) { delete config.permission.freely } if (!(config.defaultPermission in config.permission)) { @@ -110,13 +111,21 @@ for (let i = keys.length; i--;) { // and the config with uppercase is not set // we set the new config using the old key. if (uppercase.test(keys[i]) && - config[lowercaseKey] && - !config[keys[1]]) { + config[lowercaseKey] !== undefined && + fileConfig[keys[i]] === undefined) { logger.warn('config.js contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.js file to replace ' + lowercaseKey + ' with ' + keys[i]) config[keys[i]] = config[lowercaseKey] } } +// Generate session secret if it stays on default values +if (config.sessionSecret === 'secret') { + logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.') + config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number + .toString('hex') // convert to hexadecimal format + .slice(0, config.sessionSecretLen) // return required number of characters +} + // Validate upload upload providers if (['filesystem', 's3', 'minio', 'imgur'].indexOf(config.imageUploadType) === -1) { logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio" or "imgur". Defaulting to "imgur"') @@ -5,7 +5,7 @@ var CspStrategy = {} var defaultDirectives = { defaultSrc: ['\'self\''], - scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', 'https://*.disqus.com', '\'unsafe-eval\''], + scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''], // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/hackmd/issues/594 imgSrc: ['*'], styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views @@ -22,11 +22,23 @@ var cdnDirectives = { fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] } +var disqusDirectives = { + scriptSrc: ['https://*.disqus.com', 'https://*.disquscdn.com'], + styleSrc: ['https://*.disquscdn.com'], + fontSrc: ['https://*.disquscdn.com'] +} + +var googleAnalyticsDirectives = { + scriptSrc: ['https://www.google-analytics.com'] +} + CspStrategy.computeDirectives = function () { var directives = {} mergeDirectives(directives, config.csp.directives) mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives) mergeDirectivesIf(config.useCDN, directives, cdnDirectives) + mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives) + mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives) if (!areAllInlineScriptsAllowed(directives)) { addInlineScriptExceptions(directives) } diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index 7ba336b6..b5b1d9e7 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -1,16 +1,17 @@ 'use strict' // external modules -var randomcolor = require('randomcolor') +const randomcolor = require('randomcolor') +const config = require('./config') // core -module.exports = function (name) { - var color = randomcolor({ +exports.generateAvatar = function (name) { + const color = randomcolor({ seed: name, luminosity: 'dark' }) - var letter = name.substring(0, 1).toUpperCase() + const letter = name.substring(0, 1).toUpperCase() - var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' + let svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">' svg += '<g>' svg += '<rect width="96" height="96" fill="' + color + '" />' @@ -20,5 +21,9 @@ module.exports = function (name) { svg += '</g>' svg += '</svg>' - return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64') + return svg +} + +exports.generateAvatarURL = function (name) { + return config.serverURL + '/user/' + name + '/avatar.svg' } diff --git a/lib/models/note.js b/lib/models/note.js index 69393dd4..2a048e37 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -211,6 +211,15 @@ module.exports = function (sequelize, DataTypes) { }, // parse note id by LZString is deprecated, here for compability parseNoteIdByLZString: function (_callback) { + // Calculate minimal string length for an UUID that is encoded + // base64 encoded and optimize comparsion by using -1 + // this should make a lot of LZ-String parsing errors obsolete + // as we can assume that a nodeId that is 48 chars or longer is a + // noteID. + const base64UuidLength = ((4 * 36) / 3) - 1 + if (!(noteId.length > base64UuidLength)) { + return _callback(null, null) + } // try to parse note id by LZString Base64 try { var id = LZString.decompressFromBase64(noteId) diff --git a/lib/models/user.js b/lib/models/user.js index f421fe43..4c823355 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -6,7 +6,7 @@ var scrypt = require('scrypt') // core var logger = require('../logger') -var letterAvatars = require('../letter-avatars') +var {generateAvatarURL} = require('../letter-avatars') module.exports = function (sequelize, DataTypes) { var User = sequelize.define('User', { @@ -108,7 +108,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') else photo = photo.replace(/(\?s=)\d*$/i, '$196') } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'mattermost': @@ -117,7 +117,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') else photo = photo.replace(/(\?s=)\d*$/i, '$196') } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'dropbox': @@ -140,7 +140,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo += '?s=400' else photo += '?s=96' } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'saml': @@ -149,7 +149,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo += '?s=400' else photo += '?s=96' } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break } diff --git a/lib/realtime.js b/lib/realtime.js index d8b0b4c5..070bde2d 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -788,7 +788,7 @@ function connection (socket) { var note = notes[noteId] // Only owner can change permission if (note.owner && note.owner === socket.request.user.id) { - if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousedits) return + if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return note.permission = permission models.Note.update({ permission: permission diff --git a/lib/response.js b/lib/response.js index aeb594a3..d6fb3b42 100644 --- a/lib/response.js +++ b/lib/response.js @@ -65,7 +65,7 @@ function showIndex (req, res, next) { url: config.serverURL, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, - allowAnonymousEdits: config.allowAnonymousedits, + allowAnonymousEdits: config.allowAnonymousEdits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, @@ -100,7 +100,7 @@ function responseHackMD (res, note) { title: title, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, - allowAnonymousEdits: config.allowAnonymousedits, + allowAnonymousEdits: config.allowAnonymousEdits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, @@ -232,7 +232,8 @@ function showPublishNote (req, res, next) { lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, robots: meta.robots || false, // default allow robots GA: meta.GA, - disqus: meta.disqus + disqus: meta.disqus, + cspNonce: res.locals.nonce } return renderPublish(data, res) }).catch(function (err) { diff --git a/lib/web/auth/saml/index.js b/lib/web/auth/saml/index.js index 3ecbc6f3..b8d98340 100644 --- a/lib/web/auth/saml/index.js +++ b/lib/web/auth/saml/index.js @@ -20,14 +20,14 @@ passport.use(new SamlStrategy({ identifierFormat: config.saml.identifierFormat }, function (user, done) { // check authorization if needed - if (config.saml.externalGroups && config.saml.grouptAttribute) { + if (config.saml.externalGroups && config.saml.groupAttribute) { var externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) if (externalGroups.length > 0) { logger.error('saml permission denied: ' + externalGroups.join(', ')) return done('Permission denied', null) } } - if (config.saml.requiredGroups && config.saml.grouptAttribute) { + if (config.saml.requiredGroups && config.saml.groupAttribute) { if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { logger.error('saml permission denied') return done('Permission denied', null) diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index ecfbaf8b..963961c7 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -5,6 +5,7 @@ const Router = require('express').Router const response = require('../response') const models = require('../models') const logger = require('../logger') +const {generateAvatar} = require('../letter-avatars') const UserRouter = module.exports = Router() @@ -34,3 +35,9 @@ UserRouter.get('/me', function (req, res) { }) } }) + +UserRouter.get('/user/:username/avatar.svg', function (req, res, next) { + res.setHeader('Content-Type', 'image/svg+xml') + res.setHeader('Cache-Control', 'public, max-age=86400') + res.send(generateAvatar(req.params.username)) +}) diff --git a/locales/de.json b/locales/de.json index c416a684..b2539108 100644 --- a/locales/de.json +++ b/locales/de.json @@ -62,7 +62,7 @@ "Refresh": "Neu laden", "Contacts": "Kontakte", "Report an issue": "Fehlerbericht senden", - "Meet us on Gitter": "Triff uns auf Gitter", + "Meet us on %s": "Triff uns auf %s", "Send us email": "Kontakt", "Documents": "Dokumente", "Features": "Funktionen", diff --git a/locales/en.json b/locales/en.json index 0b44732d..1aef3f6d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -62,7 +62,7 @@ "Refresh": "Refresh", "Contacts": "Contacts", "Report an issue": "Report an issue", - "Meet us on Gitter": "Meet us on Gitter", + "Meet us on %s": "Meet us on %s", "Send us email": "Send us email", "Documents": "Documents", "Features": "Features", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 6e0c11e9..87907189 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -60,7 +60,7 @@ "Refresh": "重新整理", "Contacts": "联络方式", "Report an issue": "报告问题", - "Meet us on Gitter": "在 Gitter 上联系我们", + "Meet us on %s": "在 %s 上联系我们", "Send us email": "寄信给我们", "Documents": "文件", "Features": "功能简介", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index da50f66a..090af9c0 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -60,7 +60,7 @@ "Refresh": "重新整理", "Contacts": "聯絡方式", "Report an issue": "回報問題", - "Meet us on Gitter": "透過 Gitter 聯絡我們", + "Meet us on %s": "透過 %s 聯絡我們", "Send us email": "寄信給我們", "Documents": "文件", "Features": "功能簡介", diff --git a/package.json b/package.json index 58e2afff..c31b3c8a 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "hackmd", - "version": "1.0.1-ce", + "version": "1.1.1-ce", "description": "Realtime collaborative markdown notes on all platforms.", "main": "app.js", "license": "AGPL-3.0", "scripts": { "test": "npm run-script standard && npm run-script jsonlint", - "jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' | while read json; do echo $json ; jq . $json; done", + "jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done", "standard": "node ./node_modules/standard/bin/cmd.js", "dev": "webpack --config webpack.config.js --progress --colors --watch", "build": "webpack --config webpack.production.js --progress --colors --bail", @@ -18,7 +18,7 @@ "Idle.Js": "git+https://github.com/shawnmclean/Idle.js", "async": "^2.1.4", "aws-sdk": "^2.7.20", - "base64url": "^2.0.0", + "base64url": "^3.0.0", "blueimp-md5": "^2.6.0", "body-parser": "^1.15.2", "bootstrap": "^3.3.7", @@ -42,6 +42,7 @@ "font-awesome": "^4.7.0", "formidable": "^1.0.17", "gist-embed": "~2.6.0", + "graceful-fs": "^4.1.11", "handlebars": "^4.0.6", "helmet": "^3.3.0", "highlight.js": "~9.9.0", @@ -131,7 +132,7 @@ "xss": "^0.3.3" }, "engines": { - "node": ">=6.x" + "node": ">=6.x <10.x" }, "bugs": "https://github.com/hackmdio/hackmd/issues", "keywords": [ diff --git a/public/css/index.css b/public/css/index.css index b00eba41..3f391e27 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -156,6 +156,10 @@ body.night{ left: 50%; transform: translate(-50%, -50%); } +.night .ui-edit-area .ui-sync-toggle { + box-shadow: 2px 0px 2px #353535; +} + .ui-edit-area .ui-sync-toggle:active { box-shadow: inset 0 3px 5px rgba(0,0,0,.125), 2px 0px 2px #e7e7e7; } @@ -292,6 +296,13 @@ body.night{ background: #222; } +.night .modal-content, +.night .panel, +.night .panel-heading { + color: #eee; + background-color: #333; +} + .dropdown-menu.CodeMirror-other-cursor { transition: none; } @@ -340,7 +351,8 @@ div[contenteditable]:empty:not(:focus):before{ background: inherit; } -.night .navbar .btn-default{ +.night .navbar .btn-default, +.night .close { background-color: #333; border-color: #565656; color: #eee; @@ -372,8 +384,10 @@ div[contenteditable]:empty:not(:focus):before{ .night .btn.focus, .night .btn:focus, -.night .btn:hover{ +.night .btn:hover, +.night .close { color: #fff; + background-color: #333; } .info-label { diff --git a/public/css/markdown.css b/public/css/markdown.css index eaa9ab5c..85a4c594 100644 --- a/public/css/markdown.css +++ b/public/css/markdown.css @@ -13,6 +13,10 @@ border: inherit !important; } +.night .markdown-body pre { + filter: invert(100%); +} + .markdown-body code { color: inherit !important; } @@ -78,6 +82,7 @@ .markdown-body code[data-gist-id] { background: none; padding: 0; + filter: invert(100%); } .markdown-body code[data-gist-id]:before { diff --git a/public/docs/features.md b/public/docs/features.md index 01340fd7..dc6ddafa 100644 --- a/public/docs/features.md +++ b/public/docs/features.md @@ -8,7 +8,7 @@ This means that you can write notes with other people on your **desktop**, **tab You can sign-in via multiple auth providers like **Facebook**, **Twitter**, **GitHub** and many more on the [_homepage_](/). If you experience any _issues_, feel free to report it on [**GitHub**](https://github.com/hackmdio/hackmd/issues). -Or meet us on [**Gitter**](https://gitter.im/hackmdio/hackmd) for dev-talk and interactive help. +Or meet us on [**Matrix.org**](https://riot.im/app/#/room/#hackmd:matrix.org) or [**Gitter**](https://gitter.im/hackmdio/hackmd) for dev-talk and interactive help. **Thank you very much!** Workspace @@ -25,11 +25,16 @@ Workspace <i class="fa fa-toggle-on fa-fw"></i> View: See only the result. <i class="fa fa-toggle-off fa-fw"></i> Edit: See only the editor. +## Night Mode: +When you are tired of a white screen and like a night mode, click on the little moon <i class="fa fa-moon-o"></i> and turn on the night view of HackMD. + +The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun<i class="fa fa-sun-o fa-fw"></i>. + ## Image Upload: You can upload an image simply by clicking on the camera button <i class="fa fa-camera"></i>. Alternatively, you can **drag-n-drop** an image into the editor. Even **pasting** images is possible! -This will automatically upload the image to **[imgur](http://imgur.com)**, nothing to worry. :tada: -![](https://i.imgur.com/9cgQVqD.png) +This will automatically upload the image to **[imgur](http://imgur.com)**, **[Amazon S3](https://aws.amazon.com/s3/)**, **[Minio](https://minio.io)** or **local filesystem**, nothing to worry about. :tada: +![imgur](https://i.imgur.com/9cgQVqD.png) ## Share Notes: If you want to share an **editable** note, just copy the URL. diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index 70510b19..891c506a 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -1,6 +1,91 @@ Release Notes === +<i class="fa fa-tag"></i> 1.1.1-ce <i class="fa fa-clock-o"></i> 2018-05-23 12:00 +--- + +### Security +* Fix Google Drive integration leaked `clientSecret` for Google integration +* Update base64url package + +### Fixes +* Fix typos in integrations +* Fix high need of file descriptors during build +* Fix heroku deployment by limiting node version to <10.x + +### Refactors +* Refactor letterAvatars to be compliant with CSP + +### Removes +* Google Drive integration + +### Honorable mentions +* [Max Wu (jackycute)](https://github.com/jackycute) + +<i class="fa fa-tag"></i> 1.1.0-ce <i class="fa fa-clock-o"></i> 2018-04-06 12:00 +--- + +### Security +* Adding CSP headers +* Prevent data-leak by wrong LDAP config +* Generate dynamic `sessionSecret` if none is specified + +### Enhancements +* Add Minio support +* Allow posting content to new notes by API +* Add anonymous edit function in restricted mode +* Add support for more Mimetypes on S3, Minio and local filesystem uploads +* Add basic CLI tooling for local user management +* Add referrer policy +* Add more usable HTML5 tags +* Add `useridField` in LDAP config +* Add option for ReportURI for CSP violations +* Add persistance for night mode +* Allow setting of `sessionSecret` by environment variable +* Add night mode to features page +* Add Riot / Matrix - Community link to help page + +### Fixes +* Fix ToDo-toggle function +* Fix LDAP provider name in front-end +* Fix errors on authenticated sessions for deleted users +* Fix typo in database migration +* Fix possible data truncation of authorship +* Minor fixes in README.md +* Allow usage of ESC-key by codemirror +* Fix array of emails in LDAP +* Fix type errors by environment configs +* Fix error message on some file API errors +* Fix minor CSS issues in night mode + +### Refactors +* Refactor contact +* Refactor social media integration on main page +* Refactor socket.io code to no longer use referrer +* Refactor webpack config to need less dependencies in package.json +* Refactor imageRouter for modularity +* Refactor configs to be camel case + +### Removes +* Remove unused `tokenSecret` from LDAP config + +### Deprecations +* All non-camelcase config + +### Honorable mentions +* [Dario Ernst (Nebukadneza)](https://github.com/Nebukadneza) +* [David Mehren (davidmehren)](https://github.com/davidmehren) +* [Dustin Frisch (fooker)](https://github.com/fooker) +* [Felix Schäfer (thegcat)](https://github.com/thegcat) +* [Literallie (xxyy)](https://github.com/xxyy) +* [Marc Deop (marcdeop)](https://github.com/marcdeop) +* [Max Wu (jackycute)](https://github.com/jackycute) +* [Robin Naundorf (senk)](https://github.com/senk) +* [Stefan Bühler (stbuehler)](https://github.com/stbuehler) +* [Takeaki Matsumoto (takmatsu)](https://github.com/takmatsu) +* [Tang TsungYi (vazontang)](https://github.com/vazontang) +* [Zearin (Zearin)](https://github.com/Zearin) + <i class="fa fa-tag"></i> 1.0.1-ce <i class="fa fa-clock-o"></i> 2018-01-19 15:00 --- @@ -46,7 +131,7 @@ Release Notes * Fix mermaid compatiblity with new version * Fix SSL CA path parsing -### Refactor +### Refactors * Refactor main page * Refactor status pages * Refactor config handling @@ -182,7 +267,7 @@ Release Notes * Fix client socket on delete event might not delete corresponding history record correctly * Fix to handle name or color is undefined error * Fix history item event not bind properly on pagination change -* Fix history time should save in UNIX timestamp to avoid time offset issue +* Fix history time should save in UNIX timestamp to avoid time offset issue ### Removes - Drop bower the package manager @@ -230,16 +315,16 @@ Release Notes ### Fixes * Fix README and features document format and grammar issues * Fix some potential memory leaks bugs -* Fix history storage might not fallback correctly +* Fix history storage might not fallback correctly * Fix to make mathjax expression display in editor correctly (not italic) -* Fix note title might have unstriped html tags +* Fix note title might have unstriped html tags * Fix client reconnect should resend last operation * Fix a bug when setting both maxAge and expires may cause user can't signin * Fix text complete extra tags for blockquote and referrals * Fix bug that when window close will make ajax fail and cause cookies set to wrong state * Fix markdown render might fall into regex infinite loop -* Fix syntax error caused by element contain special characters -* Fix reference error caused by some scripts loading order +* Fix syntax error caused by element contain special characters +* Fix reference error caused by some scripts loading order * Fix ToC id naming to avoid possible overlap with user ToC * Fix header nav bar rwd detect element should use div tag or it might glitch the layout * Fix textcomplete of extra tags for blockquote not match space character in the between @@ -279,7 +364,7 @@ Release Notes ### Fixes * Workaround vim mode might overwrite copy keyMap on Windows * Fix TOC might not update after changeMode -* Workaround slide mode gets glitch and blurry text on Firefox 47+ +* Workaround slide mode gets glitch and blurry text on Firefox 47+ * Fix idle.js not change isAway property on onAway and onAwayBack events * Fix http body request entity too large issue * Fix google-diff-match-patch encodeURI exception issue @@ -287,8 +372,8 @@ Release Notes * Fix spellcheck settings from cookies might not a boolean in string type * Fix cookies might not in boolean type cause page refresh loop * Fix the signin and logout redirect url might be empty -* Fix realtime might not clear or remove invalid sockets in queue -* Fix slide not refresh layout on ajax item loaded +* Fix realtime might not clear or remove invalid sockets in queue +* Fix slide not refresh layout on ajax item loaded * Fix retryOnDisconnect not clean up after reconnected * Fix some potential memory leaks @@ -342,7 +427,7 @@ Release Notes * Support maintenance mode and gracefully exit process on signal * Update to update doc in db when doc in filesystem have newer modified time * Update to replace animation acceleration library from gsap to velocity -* Support image syntax with size +* Support image syntax with size * Update textcomplete rules to support more conditions * Update to use bigger user profile image * Support showing signin button only when needed diff --git a/public/js/google-drive-picker.js b/public/js/google-drive-picker.js deleted file mode 100644 index 5006cd25..00000000 --- a/public/js/google-drive-picker.js +++ /dev/null @@ -1,118 +0,0 @@ -/** ! - * Google Drive File Picker Example - * By Daniel Lo Nigro (http://dan.cx/) - */ -(function () { - /** - * Initialise a Google Driver file picker - */ - var FilePicker = window.FilePicker = function (options) { - // Config - this.apiKey = options.apiKey - this.clientId = options.clientId - - // Elements - this.buttonEl = options.buttonEl - - // Events - this.onSelect = options.onSelect - this.buttonEl.on('click', this.open.bind(this)) - - // Disable the button until the API loads, as it won't work properly until then. - this.buttonEl.prop('disabled', true) - - // Load the drive API - window.gapi.client.setApiKey(this.apiKey) - window.gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this)) - window.google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) }) - } - - FilePicker.prototype = { - /** - * Open the file picker. - */ - open: function () { - // Check if the user has already authenticated - var token = window.gapi.auth.getToken() - if (token) { - this._showPicker() - } else { - // The user has not yet authenticated with Google - // We need to do the authentication before displaying the Drive picker. - this._doAuth(false, function () { this._showPicker() }.bind(this)) - } - }, - - /** - * Show the file picker once authentication has been done. - * @private - */ - _showPicker: function () { - var accessToken = window.gapi.auth.getToken().access_token - var view = new window.google.picker.DocsView() - view.setMimeTypes('text/markdown,text/html') - view.setIncludeFolders(true) - view.setOwnedByMe(true) - this.picker = new window.google.picker.PickerBuilder() - .enableFeature(window.google.picker.Feature.NAV_HIDDEN) - .addView(view) - .setAppId(this.clientId) - .setOAuthToken(accessToken) - .setCallback(this._pickerCallback.bind(this)) - .build() - .setVisible(true) - }, - - /** - * Called when a file has been selected in the Google Drive file picker. - * @private - */ - _pickerCallback: function (data) { - if (data[window.google.picker.Response.ACTION] === window.google.picker.Action.PICKED) { - var file = data[window.google.picker.Response.DOCUMENTS][0] - var id = file[window.google.picker.Document.ID] - var request = window.gapi.client.drive.files.get({ - fileId: id - }) - request.execute(this._fileGetCallback.bind(this)) - } - }, - /** - * Called when file details have been retrieved from Google Drive. - * @private - */ - _fileGetCallback: function (file) { - if (this.onSelect) { - this.onSelect(file) - } - }, - - /** - * Called when the Google Drive file picker API has finished loading. - * @private - */ - _pickerApiLoaded: function () { - this.buttonEl.prop('disabled', false) - }, - - /** - * Called when the Google Drive API has finished loading. - * @private - */ - _driveApiLoaded: function () { - this._doAuth(true) - }, - - /** - * Authenticate with Google Drive via the Google JavaScript API. - * @private - */ - _doAuth: function (immediate, callback) { - window.gapi.auth.authorize({ - client_id: this.clientId, - scope: 'https://www.googleapis.com/auth/drive.readonly', - immediate: immediate - }, callback || function () {}) - } - } -}()) diff --git a/public/js/google-drive-upload.js b/public/js/google-drive-upload.js deleted file mode 100644 index 6c0e8a62..00000000 --- a/public/js/google-drive-upload.js +++ /dev/null @@ -1,267 +0,0 @@ -/* eslint-env browser, jquery */ -/** - * Helper for implementing retries with backoff. Initial retry - * delay is 1 second, increasing by 2x (+jitter) for subsequent retries - * - * @constructor - */ -var RetryHandler = function () { - this.interval = 1000 // Start at one second - this.maxInterval = 60 * 1000 // Don't wait longer than a minute -} - -/** - * Invoke the function after waiting - * - * @param {function} fn Function to invoke - */ -RetryHandler.prototype.retry = function (fn) { - setTimeout(fn, this.interval) - this.interval = this.nextInterval_() -} - -/** - * Reset the counter (e.g. after successful request.) - */ -RetryHandler.prototype.reset = function () { - this.interval = 1000 -} - -/** - * Calculate the next wait time. - * @return {number} Next wait interval, in milliseconds - * - * @private - */ -RetryHandler.prototype.nextInterval_ = function () { - var interval = this.interval * 2 + this.getRandomInt_(0, 1000) - return Math.min(interval, this.maxInterval) -} - -/** - * Get a random int in the range of min to max. Used to add jitter to wait times. - * - * @param {number} min Lower bounds - * @param {number} max Upper bounds - * @private - */ -RetryHandler.prototype.getRandomInt_ = function (min, max) { - return Math.floor(Math.random() * (max - min + 1) + min) -} - -/** - * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether - * files or in-memory constructs. - * - * @example - * var content = new Blob(["Hello world"], {"type": "text/plain"}); - * var uploader = new MediaUploader({ - * file: content, - * token: accessToken, - * onComplete: function(data) { ... } - * onError: function(data) { ... } - * }); - * uploader.upload(); - * - * @constructor - * @param {object} options Hash of options - * @param {string} options.token Access token - * @param {blob} options.file Blob-like item to upload - * @param {string} [options.fileId] ID of file if replacing - * @param {object} [options.params] Additional query parameters - * @param {string} [options.contentType] Content-type, if overriding the type of the blob. - * @param {object} [options.metadata] File metadata - * @param {function} [options.onComplete] Callback for when upload is complete - * @param {function} [options.onProgress] Callback for status for the in-progress upload - * @param {function} [options.onError] Callback if upload fails - */ -var MediaUploader = function (options) { - var noop = function () {} - this.file = options.file - this.contentType = options.contentType || this.file.type || 'application/octet-stream' - this.metadata = options.metadata || { - 'title': this.file.name, - 'mimeType': this.contentType - } - this.token = options.token - this.onComplete = options.onComplete || noop - this.onProgress = options.onProgress || noop - this.onError = options.onError || noop - this.offset = options.offset || 0 - this.chunkSize = options.chunkSize || 0 - this.retryHandler = new RetryHandler() - - this.url = options.url - if (!this.url) { - var params = options.params || {} - params.uploadType = 'resumable' - this.url = this.buildUrl_(options.fileId, params, options.baseUrl) - } - this.httpMethod = options.fileId ? 'PUT' : 'POST' -} - -/** - * Initiate the upload. - */ -MediaUploader.prototype.upload = function () { - var xhr = new XMLHttpRequest() - - xhr.open(this.httpMethod, this.url, true) - xhr.setRequestHeader('Authorization', 'Bearer ' + this.token) - xhr.setRequestHeader('Content-Type', 'application/json') - xhr.setRequestHeader('X-Upload-Content-Length', this.file.size) - xhr.setRequestHeader('X-Upload-Content-Type', this.contentType) - - xhr.onload = function (e) { - if (e.target.status < 400) { - var location = e.target.getResponseHeader('Location') - this.url = location - this.sendFile_() - } else { - this.onUploadError_(e) - } - }.bind(this) - xhr.onerror = this.onUploadError_.bind(this) - xhr.send(JSON.stringify(this.metadata)) -} - -/** - * Send the actual file content. - * - * @private - */ -MediaUploader.prototype.sendFile_ = function () { - var content = this.file - var end = this.file.size - - if (this.offset || this.chunkSize) { - // Only bother to slice the file if we're either resuming or uploading in chunks - if (this.chunkSize) { - end = Math.min(this.offset + this.chunkSize, this.file.size) - } - content = content.slice(this.offset, end) - } - - var xhr = new XMLHttpRequest() - xhr.open('PUT', this.url, true) - xhr.setRequestHeader('Content-Type', this.contentType) - xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size) - xhr.setRequestHeader('X-Upload-Content-Type', this.file.type) - if (xhr.upload) { - xhr.upload.addEventListener('progress', this.onProgress) - } - xhr.onload = this.onContentUploadSuccess_.bind(this) - xhr.onerror = this.onContentUploadError_.bind(this) - xhr.send(content) -} - -/** - * Query for the state of the file for resumption. - * - * @private - */ -MediaUploader.prototype.resume_ = function () { - var xhr = new XMLHttpRequest() - xhr.open('PUT', this.url, true) - xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size) - xhr.setRequestHeader('X-Upload-Content-Type', this.file.type) - if (xhr.upload) { - xhr.upload.addEventListener('progress', this.onProgress) - } - xhr.onload = this.onContentUploadSuccess_.bind(this) - xhr.onerror = this.onContentUploadError_.bind(this) - xhr.send() -} - -/** - * Extract the last saved range if available in the request. - * - * @param {XMLHttpRequest} xhr Request object - */ -MediaUploader.prototype.extractRange_ = function (xhr) { - var range = xhr.getResponseHeader('Range') - if (range) { - this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1 - } -} - -/** - * Handle successful responses for uploads. Depending on the context, - * may continue with uploading the next chunk of the file or, if complete, - * invokes the caller's callback. - * - * @private - * @param {object} e XHR event - */ -MediaUploader.prototype.onContentUploadSuccess_ = function (e) { - if (e.target.status === 200 || e.target.status === 201) { - this.onComplete(e.target.response) - } else if (e.target.status === 308) { - this.extractRange_(e.target) - this.retryHandler.reset() - this.sendFile_() - } else { - this.onContentUploadError_(e) - } -} - -/** - * Handles errors for uploads. Either retries or aborts depending - * on the error. - * - * @private - * @param {object} e XHR event - */ -MediaUploader.prototype.onContentUploadError_ = function (e) { - if (e.target.status && e.target.status < 500) { - this.onError(e.target.response) - } else { - this.retryHandler.retry(this.resume_.bind(this)) - } -} - -/** - * Handles errors for the initial request. - * - * @private - * @param {object} e XHR event - */ -MediaUploader.prototype.onUploadError_ = function (e) { - this.onError(e.target.response) // TODO - Retries for initial upload -} - -/** - * Construct a query string from a hash/object - * - * @private - * @param {object} [params] Key/value pairs for query string - * @return {string} query string - */ -MediaUploader.prototype.buildQuery_ = function (params) { - params = params || {} - return Object.keys(params).map(function (key) { - return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) - }).join('&') -} - -/** - * Build the drive upload URL - * - * @private - * @param {string} [id] File ID if replacing - * @param {object} [params] Query parameters - * @return {string} URL - */ -MediaUploader.prototype.buildUrl_ = function (id, params, baseUrl) { - var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/' - if (id) { - url += id - } - var query = this.buildQuery_(params) - if (query) { - url += '?' + query - } - return url -} - -window.MediaUploader = MediaUploader diff --git a/public/js/index.js b/public/js/index.js index d76a37fe..c6a4f770 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -30,8 +30,6 @@ import { import { debug, DROPBOX_APP_KEY, - GOOGLE_API_KEY, - GOOGLE_CLIENT_ID, noteid, noteurl, urlpath, @@ -908,29 +906,6 @@ if (DROPBOX_APP_KEY) { ui.toolbar.export.dropbox.hide() } -// check if google api key and client id are set and load scripts -if (GOOGLE_API_KEY && GOOGLE_CLIENT_ID) { - $('<script>') - .attr('type', 'text/javascript') - .attr('src', 'https://www.google.com/jsapi?callback=onGoogleAPILoaded') - .prop('async', true) - .prop('defer', true) - .appendTo('body') -} else { - ui.toolbar.import.googleDrive.hide() - ui.toolbar.export.googleDrive.hide() -} - -function onGoogleAPILoaded () { - $('<script>') - .attr('type', 'text/javascript') - .attr('src', 'https://apis.google.com/js/client:plusone.js?onload=onGoogleClientLoaded') - .prop('async', true) - .prop('defer', true) - .appendTo('body') -} -window.onGoogleAPILoaded = onGoogleAPILoaded - // button actions // share ui.toolbar.publish.attr('href', noteurl + '/publish') @@ -979,53 +954,6 @@ ui.toolbar.export.dropbox.click(function () { } Dropbox.save(options) }) -function uploadToGoogleDrive (accessToken) { - ui.spinner.show() - var filename = renderFilename(ui.area.markdown) + '.md' - var markdown = editor.getValue() - var blob = new Blob([markdown], { - type: 'text/markdown;charset=utf-8' - }) - blob.name = filename - var uploader = new MediaUploader({ - file: blob, - token: accessToken, - onComplete: function (data) { - data = JSON.parse(data) - showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true) - ui.spinner.hide() - }, - onError: function (data) { - showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false) - ui.spinner.hide() - } - }) - uploader.upload() -} -function googleApiAuth (immediate, callback) { - gapi.auth.authorize( - { - 'client_id': GOOGLE_CLIENT_ID, - 'scope': 'https://www.googleapis.com/auth/drive.file', - 'immediate': immediate - }, callback || function () { }) -} -function onGoogleClientLoaded () { - googleApiAuth(true) - buildImportFromGoogleDrive() -} -window.onGoogleClientLoaded = onGoogleClientLoaded -// export to google drive -ui.toolbar.export.googleDrive.click(function (e) { - var token = gapi.auth.getToken() - if (token) { - uploadToGoogleDrive(token.access_token) - } else { - googleApiAuth(false, function (result) { - uploadToGoogleDrive(result.access_token) - }) - } -}) // export to gist ui.toolbar.export.gist.attr('href', noteurl + '/gist') // export to snippet @@ -1075,38 +1003,6 @@ ui.toolbar.import.dropbox.click(function () { } Dropbox.choose(options) }) -// import from google drive -function buildImportFromGoogleDrive () { - /* eslint-disable no-unused-vars */ - let picker = new FilePicker({ - apiKey: GOOGLE_API_KEY, - clientId: GOOGLE_CLIENT_ID, - buttonEl: ui.toolbar.import.googleDrive, - onSelect: function (file) { - if (file.downloadUrl) { - ui.spinner.show() - var accessToken = gapi.auth.getToken().access_token - $.ajax({ - type: 'GET', - beforeSend: function (request) { - request.setRequestHeader('Authorization', 'Bearer ' + accessToken) - }, - url: file.downloadUrl, - success: function (data) { - if (file.fileExtension === 'html') { parseToEditor(data) } else { replaceAll(data) } - }, - error: function (data) { - showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false) - }, - complete: function () { - ui.spinner.hide() - } - }) - } - } - }) - /* eslint-enable no-unused-vars */ -} // import from gist ui.toolbar.import.gist.click(function () { // na diff --git a/public/js/lib/common/constant.ejs b/public/js/lib/common/constant.ejs index c0963635..a94b815e 100644 --- a/public/js/lib/common/constant.ejs +++ b/public/js/lib/common/constant.ejs @@ -5,6 +5,4 @@ window.version = '<%- version %>' window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %> -window.GOOGLE_API_KEY = '<%- GOOGLE_API_KEY %>' -window.GOOGLE_CLIENT_ID = '<%- GOOGLE_CLIENT_ID %>' window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' diff --git a/public/js/lib/config/index.js b/public/js/lib/config/index.js index 11e4389f..4758ffe7 100644 --- a/public/js/lib/config/index.js +++ b/public/js/lib/config/index.js @@ -1,5 +1,3 @@ -export const GOOGLE_API_KEY = window.GOOGLE_API_KEY || '' -export const GOOGLE_CLIENT_ID = window.GOOGLE_CLIENT_ID || '' export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || '' export const domain = window.domain || '' // domain name diff --git a/public/js/lib/editor/ui-elements.js b/public/js/lib/editor/ui-elements.js index 88a1e3ca..ca06d30c 100644 --- a/public/js/lib/editor/ui-elements.js +++ b/public/js/lib/editor/ui-elements.js @@ -22,13 +22,11 @@ export const getUIElements = () => ({ }, export: { dropbox: $('.ui-save-dropbox'), - googleDrive: $('.ui-save-google-drive'), gist: $('.ui-save-gist'), snippet: $('.ui-save-snippet') }, import: { dropbox: $('.ui-import-dropbox'), - googleDrive: $('.ui-import-google-drive'), gist: $('.ui-import-gist'), snippet: $('.ui-import-snippet'), clipboard: $('.ui-import-clipboard') diff --git a/public/views/hackmd/header.ejs b/public/views/hackmd/header.ejs index e179f171..21b632ce 100644 --- a/public/views/hackmd/header.ejs +++ b/public/views/hackmd/header.ejs @@ -32,13 +32,11 @@ </li> <li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a> </li> - <% if((typeof github !== 'undefined' && github) || (typeof dropbox !== 'undefined' && dropbox) || (typeof google !== 'undefined' && google) || (typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api'))) { %> + <% if((typeof github !== 'undefined' && github) || (typeof dropbox !== 'undefined' && dropbox) || (typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api'))) { %> <li class="divider"></li> <li class="dropdown-header"><%= __('Export') %></li> <li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> </li> - <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a> - </li> <% if(typeof github !== 'undefined' && github) { %> <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a> </li> @@ -52,8 +50,6 @@ <li class="dropdown-header"><%= __('Import') %></li> <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> </li> - <li role="presentation"><a role="menuitem" class="ui-import-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-download fa-fw"></i> Google Drive</a> - </li> <li role="presentation"><a role="menuitem" class="ui-import-gist" href="#" data-toggle="modal" data-target="#gistImportModal"><i class="fa fa-github fa-fw"></i> Gist</a> </li> <% if(typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api')) { %> @@ -138,13 +134,11 @@ </li> <li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a> </li> - <% if((typeof github !== 'undefined' && github) || (typeof dropbox !== 'undefined' && dropbox) || (typeof google !== 'undefined' && google) || (typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api'))) { %> + <% if((typeof github !== 'undefined' && github) || (typeof dropbox !== 'undefined' && dropbox) || (typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api'))) { %> <li class="divider"></li> <li class="dropdown-header"><%= __('Export') %></li> <li role="presentation"><a role="menuitem" class="ui-save-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> </li> - <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a> - </li> <% if(typeof github !== 'undefined' && github) { %> <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a> </li> @@ -158,8 +152,6 @@ <li class="dropdown-header"><%= __('Import') %></li> <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> </li> - <li role="presentation"><a role="menuitem" class="ui-import-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-download fa-fw"></i> Google Drive</a> - </li> <li role="presentation"><a role="menuitem" class="ui-import-gist" href="#" data-toggle="modal" data-target="#gistImportModal"><i class="fa fa-github fa-fw"></i> Gist</a> </li> <% if(typeof gitlab !== 'undefined' && gitlab && (!gitlab.scope || gitlab.scope === 'api')) { %> diff --git a/public/views/shared/disqus.ejs b/public/views/shared/disqus.ejs index cceaa85c..840d1e38 100644 --- a/public/views/shared/disqus.ejs +++ b/public/views/shared/disqus.ejs @@ -1,14 +1,13 @@ <div id="disqus_thread"></div> -<script> +<script nonce="<%= cspNonce %>"> var disqus_config = function () { this.page.identifier = window.location.pathname.split('/').slice(-1)[0]; }; (function() { var d = document, s = d.createElement('script'); - s.src = '//<%= disqus %>.disqus.com/embed.js'; + s.src = 'https://<%= disqus %>.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })(); </script> <noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript> -
\ No newline at end of file diff --git a/public/views/shared/ga.ejs b/public/views/shared/ga.ejs index 66d4acd9..27abb742 100644 --- a/public/views/shared/ga.ejs +++ b/public/views/shared/ga.ejs @@ -1,5 +1,5 @@ <% if(typeof GA !== 'undefined' && GA) { %> -<script> +<script nonce="<%= cspNonce %>"> (function (i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { @@ -10,9 +10,9 @@ a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) -})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); +})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); ga('create', '<%= GA %>', 'auto'); ga('send', 'pageview'); </script> -<% } %>
\ No newline at end of file +<% } %> diff --git a/public/views/shared/help-modal.ejs b/public/views/shared/help-modal.ejs index f5dc55c2..6bcf637e 100644 --- a/public/views/shared/help-modal.ejs +++ b/public/views/shared/help-modal.ejs @@ -17,7 +17,9 @@ <div class="panel-body"> <a href="https://github.com/hackmdio/hackmd/issues" target="_blank"><i class="fa fa-tag fa-fw"></i> <%= __('Report an issue') %></a> <br> - <a href="https://gitter.im/hackmdio/hackmd" target="_blank"><i class="fa fa-comments fa-fw"></i> <%= __('Meet us on Gitter') %></a> + <a href="https://riot.im/app/#/room/#hackmd:matrix.org" target="_blank"><i class="fa fa-hashtag fa-fw"></i> <%= __('Meet us on %s', 'Matrix') %></a> + <br> + <a href="https://gitter.im/hackmdio/hackmd" target="_blank"><i class="fa fa-comments fa-fw"></i> <%= __('Meet us on %s', 'Gitter') %></a> </div> </div> <div class="panel panel-default"> diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js index e8630841..2c6a56f6 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -4,6 +4,11 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin') var CopyWebpackPlugin = require('copy-webpack-plugin') +// Fix possible nofile-issues +var fs = require('fs') +var gracefulFs = require('graceful-fs') +gracefulFs.gracefulify(fs) + module.exports = { plugins: [ new webpack.ProvidePlugin({ @@ -203,8 +208,6 @@ module.exports = { 'flowchart.js', 'js-sequence-diagrams', 'expose?RevealMarkdown!reveal-markdown', - path.join(__dirname, 'public/js/google-drive-upload.js'), - path.join(__dirname, 'public/js/google-drive-picker.js'), path.join(__dirname, 'public/js/index.js') ], 'index-styles': [ @@ -261,8 +264,6 @@ module.exports = { 'script!abcjs', 'expose?io!socket.io-client', 'expose?RevealMarkdown!reveal-markdown', - path.join(__dirname, 'public/js/google-drive-upload.js'), - path.join(__dirname, 'public/js/google-drive-picker.js'), path.join(__dirname, 'public/js/index.js') ], pretty: [ @@ -818,9 +818,9 @@ base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" -base64url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" +base64url@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.0.tgz#f2ba30b15f80413d88e3e6116c4f3f7f61e28a2a" basic-auth@~2.0.0: version "2.0.0" @@ -2968,7 +2968,7 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" -graceful-fs@*, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@*, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" |