summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md18
-rw-r--r--app.js2
-rw-r--r--app.json8
-rw-r--r--config.json.example8
-rw-r--r--docs/guides/auth.md29
-rw-r--r--docs/guides/images/auth/gitlab-application-details.pngbin0 -> 30378 bytes
-rw-r--r--docs/guides/images/auth/gitlab-new-application.pngbin0 -> 45457 bytes
-rw-r--r--docs/guides/images/auth/gitlab-sign-in.pngbin0 -> 5590 bytes
-rw-r--r--lib/config/default.js3
-rw-r--r--lib/config/defaultSSL.js8
-rw-r--r--lib/config/environment.js2
-rw-r--r--lib/config/index.js15
-rw-r--r--lib/csp.js14
-rw-r--r--lib/letter-avatars.js17
-rw-r--r--lib/models/note.js9
-rw-r--r--lib/models/user.js10
-rw-r--r--lib/realtime.js2
-rw-r--r--lib/response.js7
-rw-r--r--lib/web/auth/saml/index.js4
-rw-r--r--lib/web/userRouter.js7
-rw-r--r--locales/de.json2
-rw-r--r--locales/en.json2
-rw-r--r--locales/zh-CN.json2
-rw-r--r--locales/zh-TW.json2
-rw-r--r--package.json9
-rw-r--r--public/css/index.css18
-rw-r--r--public/css/markdown.css5
-rw-r--r--public/docs/features.md11
-rw-r--r--public/docs/release-notes.md105
-rw-r--r--public/js/google-drive-picker.js118
-rw-r--r--public/js/google-drive-upload.js267
-rw-r--r--public/js/index.js104
-rw-r--r--public/js/lib/common/constant.ejs2
-rw-r--r--public/js/lib/config/index.js2
-rw-r--r--public/js/lib/editor/ui-elements.js2
-rw-r--r--public/views/hackmd/header.ejs12
-rw-r--r--public/views/shared/disqus.ejs5
-rw-r--r--public/views/shared/ga.ejs6
-rw-r--r--public/views/shared/help-modal.ejs4
-rw-r--r--webpackBaseConfig.js9
-rw-r--r--yarn.lock8
41 files changed, 278 insertions, 580 deletions
diff --git a/README.md b/README.md
index 0bb3845b..f302802c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app.js b/app.js
index fcf905d5..8f9300f2 100644
--- a/app.js
+++ b/app.js
@@ -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
}
diff --git a/app.json b/app.json
index b2116eb6..36877e68 100644
--- a/app.json
+++ b/app.json
@@ -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
new file mode 100644
index 00000000..6e042886
--- /dev/null
+++ b/docs/guides/images/auth/gitlab-application-details.png
Binary files differ
diff --git a/docs/guides/images/auth/gitlab-new-application.png b/docs/guides/images/auth/gitlab-new-application.png
new file mode 100644
index 00000000..be9e4446
--- /dev/null
+++ b/docs/guides/images/auth/gitlab-new-application.png
Binary files differ
diff --git a/docs/guides/images/auth/gitlab-sign-in.png b/docs/guides/images/auth/gitlab-sign-in.png
new file mode 100644
index 00000000..27aaf6dd
--- /dev/null
+++ b/docs/guides/images/auth/gitlab-sign-in.png
Binary files differ
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"')
diff --git a/lib/csp.js b/lib/csp.js
index 8a4aa088..d0f906a3 100644
--- a/lib/csp.js
+++ b/lib/csp.js
@@ -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: [
diff --git a/yarn.lock b/yarn.lock
index 3246fa99..a2c87d2c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"