summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--app.js5
-rw-r--r--config.json.example7
-rw-r--r--docs/guides/auth.md241
-rw-r--r--docs/guides/auth/github.md36
-rw-r--r--docs/guides/auth/gitlab-self-hosted.md30
-rw-r--r--docs/guides/auth/saml-onelogin.md52
-rw-r--r--docs/guides/auth/saml.md83
-rw-r--r--docs/guides/auth/twitter.md42
-rw-r--r--lib/config/default.js12
-rw-r--r--lib/config/dockerSecret.js3
-rw-r--r--lib/config/environment.js15
-rw-r--r--lib/config/index.js8
-rw-r--r--lib/migrations/20180326103000-use-text-in-tokens.js23
-rw-r--r--lib/models/user.js4
-rw-r--r--lib/response.js12
-rw-r--r--lib/web/auth/index.js1
-rw-r--r--lib/web/auth/oauth2/index.js106
-rw-r--r--lib/web/imageRouter/azure.js35
-rw-r--r--lib/web/imageRouter/filesystem.js3
-rw-r--r--lib/web/imageRouter/imgur.js2
-rw-r--r--lib/web/imageRouter/minio.js3
-rw-r--r--lib/web/imageRouter/s3.js3
-rw-r--r--locales/ko.json109
-rw-r--r--package.json3
-rw-r--r--public/css/slide.css3
-rw-r--r--public/docs/release-notes.md21
-rw-r--r--public/vendor/md-toc.js3
-rw-r--r--public/views/index/body.ejs5
-rw-r--r--public/views/shared/signin-modal.ejs9
-rw-r--r--yarn.lock196
31 files changed, 816 insertions, 265 deletions
diff --git a/README.md b/README.md
index f302802c..ba041825 100644
--- a/README.md
+++ b/README.md
@@ -212,6 +212,8 @@ There are some config settings you need to change in the files below.
| `HMD_MINIO_ENDPOINT` | `minio.example.org` | Address of your Minio endpoint/instance |
| `HMD_MINIO_PORT` | `9000` | Port that is used for your Minio instance |
| `HMD_MINIO_SECURE` | `true` | If set to `true` HTTPS is used for Minio |
+| `HMD_AZURE_CONNECTION_STRING` | no example | Azure Blob Storage connection string |
+| `HMD_AZURE_CONTAINER` | no example | Azure Blob Storage container name (automatically created if non existent) |
| `HMD_HSTS_ENABLE` | ` true` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) |
| `HMD_HSTS_INCLUDE_SUBDOMAINS` | `true` | set to include subdomains in HSTS (default is `true`) |
| `HMD_HSTS_MAX_AGE` | `31536000` | max duration in seconds to tell clients to keep HSTS status (default is a year) |
@@ -261,7 +263,7 @@ There are some config settings you need to change in the files below.
| `documentMaxLength` | `100000` | note max length |
| `email` | `true` or `false` | set to allow email signin |
| `allowEmailRegister` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) |
-| `imageUploadType` | `imgur`(default), `s3`, `minio` or `filesystem` | Where to upload image
+| `imageUploadType` | `imgur`(default), `s3`, `minio`, `azure` or `filesystem` | Where to upload image
| `minio` | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also checkout our [Minio Image Upload Guide](docs/guides/minio-image-upload.md) |
| `s3` | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
| `s3bucket` | `YOUR_S3_BUCKET_NAME` | bucket name when `imageUploadType` is set to `s3` or `minio` |
@@ -271,7 +273,7 @@ There are some config settings you need to change in the files below.
| service | settings location | description |
| ------- | --------- | ----------- |
| 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 |
+| imgur, s3, minio, azure | environment variables or `config.json` | for image upload |
| dropbox(`dropbox/appKey`) | `config.json` | for export and import |
## Third-party integration OAuth callback URLs
diff --git a/app.js b/app.js
index 8f9300f2..772073b0 100644
--- a/app.js
+++ b/app.js
@@ -129,9 +129,10 @@ if (config.csp.enable) {
}
i18n.configure({
- locales: ['en', 'zh', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da'],
+ locales: ['en', 'zh', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko'],
cookie: 'locale',
- directory: path.join(__dirname, '/locales')
+ directory: path.join(__dirname, '/locales'),
+ updateFiles: config.updateI18nFiles
})
app.use(cookieParser())
diff --git a/config.json.example b/config.json.example
index 6cd55efb..e07052bd 100644
--- a/config.json.example
+++ b/config.json.example
@@ -114,6 +114,11 @@
"secretAccessKey": "change this",
"region": "change this"
},
- "s3bucket": "change this"
+ "s3bucket": "change this",
+ "azure":
+ {
+ "connectionString": "change this",
+ "container": "change this"
+ }
}
}
diff --git a/docs/guides/auth.md b/docs/guides/auth.md
deleted file mode 100644
index e4261724..00000000
--- a/docs/guides/auth.md
+++ /dev/null
@@ -1,241 +0,0 @@
-# Guide - Authentication
-
-### Twitter
-1. Sign-in or sign-up for a Twitter account
-2. Go to the Twitter Application management page [here](https://apps.twitter.com/)
-3. Click on the **Create New App** button to create a new Twitter app:
-
-![create-twitter-app](images/auth/create-twitter-app.png)
-
-4. Fill out the create application form, check the developer agreement box, and click **Create Your Twitter Application**
-
-![register-twitter-application](images/auth/register-twitter-application.png)
-
-*Note: you may have to register your phone number with Twitter to create a Twitter application*
-
-To do this Click your profile icon --> Settings and privacy --> Mobile --> Select Country/region --> Enter phone number --> Click Continue
-
-5. After you receive confirmation that the Twitter application was created, click **Keys and Access Tokens**
-
-![twitter-app-confirmation](images/auth/twitter-app-confirmation.png)
-
-6. Obtain your Twitter Consumer Key and Consumer Secret
-
-![twitter-app-keys](images/auth/twitter-app-keys.png)
-
-7. Add your Consumer Key and Consumer Secret to your config.json file or pass them as environment variables:
- * config.json:
- ````javascript
- {
- "production": {
- "twitter": {
- "consumerKey": "esTCJFXXXXXXXXXXXXXXXXXXX",
- "consumerSecret": "zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
- }
- }
- }
- ````
- * environment variables:
- ````
- HMD_TWITTER_CONSUMERKEY=esTCJFXXXXXXXXXXXXXXXXXXX
- HMD_TWITTER_CONSUMERSECRET=zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- ````
-
-### GitHub
-1. Sign-in or sign-up for a GitHub account
-2. Navigate to developer settings in your GitHub account [here](https://github.com/settings/developers) and select the "OAuth Apps" tab
-3. Click on the **New OAuth App** button, to create a new OAuth App:
-
-![create-oauth-app](images/auth/create-oauth-app.png)
-
-4. Fill out the new OAuth application registration form, and click **Register Application**
-
-![register-oauth-application-form](images/auth/register-oauth-application-form.png)
-
-*Note: The callback URL is <your-hackmd-url>/auth/github/callback*
-
-5. After successfully registering the application, you'll receive the Client ID and Client Secret for the application
-
-![application-page](images/auth/application-page.png)
-
-6. Add the Client ID and Client Secret to your config.json file or pass them as environment variables
- * config.json:
- ````javascript
- {
- "production": {
- "github": {
- "clientID": "3747d30eaccXXXXXXXXX",
- "clientSecret": "2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX"
- }
- }
- }
- ````
- * environment variables:
- ````
- HMD_GITHUB_CLIENTID=3747d30eaccXXXXXXXXX
- HMD_GITHUB_CLIENTSECRET=2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX
- ````
-
-### SAML (OneLogin)
-1. Sign-in or sign-up for an OneLogin account. (available free trial for 2 weeks)
-2. Go to the administration page.
-3. Select the **APPS** menu and click on the **Add Apps**.
-
-![onelogin-add-app](images/auth/onelogin-add-app.png)
-
-4. Find "SAML Test Connector (SP)" for template of settings and select it.
-
-![onelogin-select-template](images/auth/onelogin-select-template.png)
-
-5. Edit display name and icons for OneLogin dashboard as you want, and click **SAVE**.
-
-![onelogin-edit-app-name](images/auth/onelogin-edit-app-name.png)
-
-6. After that other tabs will appear, click the **Configuration**, and fill out the below items, and click **SAVE**.
- * RelayState: The base URL of your hackmd, which is issuer. (last slash is not needed)
- * ACS (Consumer) URL Validator: The callback URL of your hackmd. (serverurl + /auth/saml/callback)
- * ACS (Consumer) URL: same as above.
- * Login URL: login URL(SAML requester) of your hackmd. (serverurl + /auth/saml)
-
-![onelogin-edit-sp-metadata](images/auth/onelogin-edit-sp-metadata.png)
-
-7. The registration is completed. Next, click **SSO** and copy or download the items below.
- * X.509 Certificate: Click **View Details** and **DOWNLOAD** or copy the content of certificate ....(A)
- * SAML 2.0 Endpoint (HTTP): Copy the URL ....(B)
-
-![onelogin-copy-idp-metadata](images/auth/onelogin-copy-idp-metadata.png)
-
-8. In your hackmd server, create IdP certificate file from (A)
-9. Add the IdP URL (B) and the Idp certificate file path to your config.json file or pass them as environment variables.
- * config.json:
- ````javascript
- {
- "production": {
- "saml": {
- "idpSsoUrl": "https://*******.onelogin.com/trust/saml2/http-post/sso/******",
- "idpCert": "/path/to/idp_cert.pem"
- }
- }
- }
- ````
- * environment variables
- ````
- HMD_SAML_IDPSSOURL=https://*******.onelogin.com/trust/saml2/http-post/sso/******
- HMD_SAML_IDPCERT=/path/to/idp_cert.pem
- ````
-10. Try sign-in with SAML from your hackmd sign-in button or OneLogin dashboard (like the screenshot below).
-
-![onelogin-use-dashboard](images/auth/onelogin-use-dashboard.png)
-
-### SAML (Other cases)
-The basic procedure is the same as the case of OneLogin which is mentioned above. If you want to match your IdP, you can use more configurations as below.
-
-* If your IdP accepts metadata XML of the service provider to ease configuraion, use this url to download metadata XML.
- * {{your-serverurl}}/auth/saml/metadata
- * _Note: If not accessable from IdP, download to local once and upload to IdP._
-* Change the value of `issuer`, `identifierFormat` to match your IdP.
- * `issuer`: A unique id to identify the application to the IdP, which is the base URL of your HackMD as default
- * `identifierFormat`: A format of unique id to identify the user of IdP, which is the format based on email address as default. It is recommend that you use as below.
- * urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress (default)
- * urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
- * config.json:
- ````javascript
- {
- "production": {
- "saml": {
- /* omitted */
- "issuer": "myhackmd"
- "identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
- }
- }
- }
- ````
- * environment variables
- ````
- HMD_SAML_ISSUER=myhackmd
- HMD_SAML_IDENTIFIERFORMAT=urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
- ````
-
-* Change mapping of attribute names to customize the displaying user name and email address to match your IdP.
- * `attribute`: A dictionary to map attribute names
- * `attribute.id`: A primary key of user table for your HackMD
- * `attribute.username`: Attribute name of displaying user name on HackMD
- * `attribute.email`: Attribute name of email address, which will be also used for Gravatar
- * _Note: Default value of all attributes is NameID of SAML response, which is email address if `identifierFormat` is default._
- * config.json:
- ````javascript
- {
- "production": {
- "saml": {
- /* omitted */
- "attribute": {
- "id": "sAMAccountName",
- "username": "displayName",
- "email": "mail"
- }
- }
- }
- }
- ````
- * environment variables
- ````
- HMD_SAML_ATTRIBUTE_ID=sAMAccountName
- HMD_SAML_ATTRIBUTE_USERNAME=nickName
- HMD_SAML_ATTRIBUTE_EMAIL=mail
- ````
-
-* If you want to controll permission by group membership, add group attribute name and required group (allowed) or external group (not allowed).
- * `groupAttribute`: An attribute name of group membership
- * `requiredGroups`: Group names array for allowed access to HackMD. Use vertical bar to separate for environment variables.
- * `externalGroups`: Group names array for not allowed access to HackMD. Use vertical bar to separate for environment variables.
- * _Note: Evaluates `externalGroups` first_
- * config.json:
- ````javascript
- {
- "production": {
- "saml": {
- /* omitted */
- "groupAttribute": "memberOf",
- "requiredGroups": [ "hackmd-users", "board-members" ],
- "externalGroups": [ "temporary-staff" ]
- }
- }
- }
- ````
- * environment variables
- ````
- HMD_SAML_GROUPATTRIBUTE=memberOf
- HMD_SAML_REQUIREDGROUPS=hackmd-users|board-members
- HMD_SAML_EXTERNALGROUPS=temporary-staff
- ````
-
-
-### 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/auth/github.md b/docs/guides/auth/github.md
new file mode 100644
index 00000000..62910cb2
--- /dev/null
+++ b/docs/guides/auth/github.md
@@ -0,0 +1,36 @@
+Authentication guide - GitHub
+===
+
+1. Sign-in or sign-up for a GitHub account
+2. Navigate to developer settings in your GitHub account [here](https://github.com/settings/developers) and select the "OAuth Apps" tab
+3. Click on the **New OAuth App** button, to create a new OAuth App:
+
+![create-oauth-app](../images/auth/create-oauth-app.png)
+
+4. Fill out the new OAuth application registration form, and click **Register Application**
+
+![register-oauth-application-form](../images/auth/register-oauth-application-form.png)
+
+*Note: The callback URL is <your-hackmd-url>/auth/github/callback*
+
+5. After successfully registering the application, you'll receive the Client ID and Client Secret for the application
+
+![application-page](../images/auth/application-page.png)
+
+6. Add the Client ID and Client Secret to your config.json file or pass them as environment variables
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "github": {
+ "clientID": "3747d30eaccXXXXXXXXX",
+ "clientSecret": "2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX"
+ }
+ }
+ }
+ ````
+ * environment variables:
+ ````
+ HMD_GITHUB_CLIENTID=3747d30eaccXXXXXXXXX
+ HMD_GITHUB_CLIENTSECRET=2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX
+ ````
diff --git a/docs/guides/auth/gitlab-self-hosted.md b/docs/guides/auth/gitlab-self-hosted.md
new file mode 100644
index 00000000..361ee958
--- /dev/null
+++ b/docs/guides/auth/gitlab-self-hosted.md
@@ -0,0 +1,30 @@
+# 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=23462a34example99XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+- HMD_GITLAB_CLIENTSECRET=5532e9dexamplXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+```
+
+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/auth/saml-onelogin.md b/docs/guides/auth/saml-onelogin.md
new file mode 100644
index 00000000..245876c5
--- /dev/null
+++ b/docs/guides/auth/saml-onelogin.md
@@ -0,0 +1,52 @@
+Authentication guide - SAML (OneLogin)
+===
+
+1. Sign-in or sign-up for an OneLogin account. (available free trial for 2 weeks)
+2. Go to the administration page.
+3. Select the **APPS** menu and click on the **Add Apps**.
+
+![onelogin-add-app](../images/auth/onelogin-add-app.png)
+
+4. Find "SAML Test Connector (SP)" for template of settings and select it.
+
+![onelogin-select-template](../images/auth/onelogin-select-template.png)
+
+5. Edit display name and icons for OneLogin dashboard as you want, and click **SAVE**.
+
+![onelogin-edit-app-name](../images/auth/onelogin-edit-app-name.png)
+
+6. After that other tabs will appear, click the **Configuration**, and fill out the below items, and click **SAVE**.
+ * RelayState: The base URL of your hackmd, which is issuer. (last slash is not needed)
+ * ACS (Consumer) URL Validator: The callback URL of your hackmd. (serverurl + /auth/saml/callback)
+ * ACS (Consumer) URL: same as above.
+ * Login URL: login URL(SAML requester) of your hackmd. (serverurl + /auth/saml)
+
+![onelogin-edit-sp-metadata](../images/auth/onelogin-edit-sp-metadata.png)
+
+7. The registration is completed. Next, click **SSO** and copy or download the items below.
+ * X.509 Certificate: Click **View Details** and **DOWNLOAD** or copy the content of certificate ....(A)
+ * SAML 2.0 Endpoint (HTTP): Copy the URL ....(B)
+
+![onelogin-copy-idp-metadata](../images/auth/onelogin-copy-idp-metadata.png)
+
+8. In your hackmd server, create IdP certificate file from (A)
+9. Add the IdP URL (B) and the Idp certificate file path to your config.json file or pass them as environment variables.
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "saml": {
+ "idpSsoUrl": "https://*******.onelogin.com/trust/saml2/http-post/sso/******",
+ "idpCert": "/path/to/idp_cert.pem"
+ }
+ }
+ }
+ ````
+ * environment variables
+ ````
+ HMD_SAML_IDPSSOURL=https://*******.onelogin.com/trust/saml2/http-post/sso/******
+ HMD_SAML_IDPCERT=/path/to/idp_cert.pem
+ ````
+10. Try sign-in with SAML from your hackmd sign-in button or OneLogin dashboard (like the screenshot below).
+
+![onelogin-use-dashboard](../images/auth/onelogin-use-dashboard.png)
diff --git a/docs/guides/auth/saml.md b/docs/guides/auth/saml.md
new file mode 100644
index 00000000..9a516675
--- /dev/null
+++ b/docs/guides/auth/saml.md
@@ -0,0 +1,83 @@
+Authentication guide - SAML
+===
+
+The basic procedure is the same as the case of OneLogin which is mentioned in [OneLogin-Guide](./saml-onelogin.md). If you want to match your IdP, you can use more configurations as below.
+
+* If your IdP accepts metadata XML of the service provider to ease configuraion, use this url to download metadata XML.
+ * {{your-serverurl}}/auth/saml/metadata
+ * _Note: If not accessable from IdP, download to local once and upload to IdP._
+* Change the value of `issuer`, `identifierFormat` to match your IdP.
+ * `issuer`: A unique id to identify the application to the IdP, which is the base URL of your HackMD as default
+ * `identifierFormat`: A format of unique id to identify the user of IdP, which is the format based on email address as default. It is recommend that you use as below.
+ * urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress (default)
+ * urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "saml": {
+ /* omitted */
+ "issuer": "myhackmd"
+ "identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ }
+ }
+ }
+ ````
+ * environment variables
+ ````
+ HMD_SAML_ISSUER=myhackmd
+ HMD_SAML_IDENTIFIERFORMAT=urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ ````
+
+* Change mapping of attribute names to customize the displaying user name and email address to match your IdP.
+ * `attribute`: A dictionary to map attribute names
+ * `attribute.id`: A primary key of user table for your HackMD
+ * `attribute.username`: Attribute name of displaying user name on HackMD
+ * `attribute.email`: Attribute name of email address, which will be also used for Gravatar
+ * _Note: Default value of all attributes is NameID of SAML response, which is email address if `identifierFormat` is default._
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "saml": {
+ /* omitted */
+ "attribute": {
+ "id": "sAMAccountName",
+ "username": "displayName",
+ "email": "mail"
+ }
+ }
+ }
+ }
+ ````
+ * environment variables
+ ````
+ HMD_SAML_ATTRIBUTE_ID=sAMAccountName
+ HMD_SAML_ATTRIBUTE_USERNAME=nickName
+ HMD_SAML_ATTRIBUTE_EMAIL=mail
+ ````
+
+* If you want to controll permission by group membership, add group attribute name and required group (allowed) or external group (not allowed).
+ * `groupAttribute`: An attribute name of group membership
+ * `requiredGroups`: Group names array for allowed access to HackMD. Use vertical bar to separate for environment variables.
+ * `externalGroups`: Group names array for not allowed access to HackMD. Use vertical bar to separate for environment variables.
+ * _Note: Evaluates `externalGroups` first_
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "saml": {
+ /* omitted */
+ "groupAttribute": "memberOf",
+ "requiredGroups": [ "hackmd-users", "board-members" ],
+ "externalGroups": [ "temporary-staff" ]
+ }
+ }
+ }
+ ````
+ * environment variables
+ ````
+ HMD_SAML_GROUPATTRIBUTE=memberOf
+ HMD_SAML_REQUIREDGROUPS=hackmd-users|board-members
+ HMD_SAML_EXTERNALGROUPS=temporary-staff
+ ````
diff --git a/docs/guides/auth/twitter.md b/docs/guides/auth/twitter.md
new file mode 100644
index 00000000..02309ca5
--- /dev/null
+++ b/docs/guides/auth/twitter.md
@@ -0,0 +1,42 @@
+Authentication guide - Twitter
+===
+
+1. Sign-in or sign-up for a Twitter account
+2. Go to the Twitter Application management page [here](https://apps.twitter.com/)
+3. Click on the **Create New App** button to create a new Twitter app:
+
+![create-twitter-app](../images/auth/create-twitter-app.png)
+
+4. Fill out the create application form, check the developer agreement box, and click **Create Your Twitter Application**
+
+![register-twitter-application](../images/auth/register-twitter-application.png)
+
+*Note: you may have to register your phone number with Twitter to create a Twitter application*
+
+To do this Click your profile icon --> Settings and privacy --> Mobile --> Select Country/region --> Enter phone number --> Click Continue
+
+5. After you receive confirmation that the Twitter application was created, click **Keys and Access Tokens**
+
+![twitter-app-confirmation](../images/auth/twitter-app-confirmation.png)
+
+6. Obtain your Twitter Consumer Key and Consumer Secret
+
+![twitter-app-keys](../images/auth/twitter-app-keys.png)
+
+7. Add your Consumer Key and Consumer Secret to your config.json file or pass them as environment variables:
+ * config.json:
+ ````javascript
+ {
+ "production": {
+ "twitter": {
+ "consumerKey": "esTCJFXXXXXXXXXXXXXXXXXXX",
+ "consumerSecret": "zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+ }
+ }
+ }
+ ````
+ * environment variables:
+ ````
+ HMD_TWITTER_CONSUMERKEY=esTCJFXXXXXXXXXXXXXXXXXXX
+ HMD_TWITTER_CONSUMERSECRET=zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ ````
diff --git a/lib/config/default.js b/lib/config/default.js
index 68849d36..1b124b3e 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -56,7 +56,7 @@ module.exports = {
heartbeatTimeout: 10000,
// document
documentMaxLength: 100000,
- // image upload setting, available options are imgur/s3/filesystem
+ // image upload setting, available options are imgur/s3/filesystem/azure
imageUploadType: 'filesystem',
imgur: {
clientID: undefined
@@ -74,7 +74,17 @@ module.exports = {
port: 9000
},
s3bucket: undefined,
+ azure: {
+ connectionString: undefined,
+ container: undefined
+ },
// authentication
+ oauth2: {
+ authorizationURL: undefined,
+ tokenURL: undefined,
+ clientID: undefined,
+ clientSecret: undefined
+ },
facebook: {
clientID: undefined,
clientSecret: undefined
diff --git a/lib/config/dockerSecret.js b/lib/config/dockerSecret.js
index b9116cd3..fd66ddfe 100644
--- a/lib/config/dockerSecret.js
+++ b/lib/config/dockerSecret.js
@@ -22,6 +22,9 @@ if (fs.existsSync(basePath)) {
accessKeyId: getSecret('s3_acccessKeyId'),
secretAccessKey: getSecret('s3_secretAccessKey')
},
+ azure: {
+ connectionString: getSecret('azure_connectionString')
+ },
facebook: {
clientID: getSecret('facebook_clientID'),
clientSecret: getSecret('facebook_clientSecret')
diff --git a/lib/config/environment.js b/lib/config/environment.js
index 3dde4786..e1c11569 100644
--- a/lib/config/environment.js
+++ b/lib/config/environment.js
@@ -45,6 +45,10 @@ module.exports = {
port: toIntegerConfig(process.env.HMD_MINIO_PORT)
},
s3bucket: process.env.HMD_S3_BUCKET,
+ azure: {
+ connectionString: process.env.HMD_AZURE_CONNECTION_STRING,
+ container: process.env.HMD_AZURE_CONTAINER
+ },
facebook: {
clientID: process.env.HMD_FACEBOOK_CLIENTID,
clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET
@@ -68,6 +72,17 @@ module.exports = {
clientID: process.env.HMD_MATTERMOST_CLIENTID,
clientSecret: process.env.HMD_MATTERMOST_CLIENTSECRET
},
+ oauth2: {
+ baseURL: process.env.HMD_OAUTH2_BASEURL,
+ userProfileURL: process.env.HMD_OAUTH2_USER_PROFILE_URL,
+ userProfileUsernameAttr: process.env.HMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
+ userProfileDisplayNameAttr: process.env.HMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
+ userProfileEmailAttr: process.env.HMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,
+ tokenURL: process.env.HMD_OAUTH2_TOKEN_URL,
+ authorizationURL: process.env.HMD_OAUTH2_AUTHORIZATION_URL,
+ clientID: process.env.HMD_OAUTH2_CLIENT_ID,
+ clientSecret: process.env.HMD_OAUTH2_CLIENT_SECRET
+ },
dropbox: {
clientID: process.env.HMD_DROPBOX_CLIENTID,
clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET,
diff --git a/lib/config/index.js b/lib/config/index.js
index bdba5e0e..7853dbad 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -99,8 +99,12 @@ config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret
config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clientSecret
config.isLDAPEnable = config.ldap.url
config.isSAMLEnable = config.saml.idpSsoUrl
+config.isOAuth2Enable = config.oauth2.clientID && config.oauth2.clientSecret
config.isPDFExportEnable = config.allowPDFExport
+// Only update i18n files in development setups
+config.updateI18nFiles = (env === Environment.development)
+
// merge legacy values
let keys = Object.keys(config)
const uppercase = /[A-Z]/
@@ -127,8 +131,8 @@ if (config.sessionSecret === 'secret') {
}
// 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"')
+if (['filesystem', 's3', 'minio', 'imgur', 'azure'].indexOf(config.imageUploadType) === -1) {
+ logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure" or "imgur". Defaulting to "imgur"')
config.imageUploadType = 'imgur'
}
diff --git a/lib/migrations/20180326103000-use-text-in-tokens.js b/lib/migrations/20180326103000-use-text-in-tokens.js
new file mode 100644
index 00000000..f9507747
--- /dev/null
+++ b/lib/migrations/20180326103000-use-text-in-tokens.js
@@ -0,0 +1,23 @@
+'use strict'
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ return queryInterface.changeColumn('Users', 'accessToken', {
+ type: Sequelize.TEXT
+ }).then(function () {
+ return queryInterface.changeColumn('Users', 'refreshToken', {
+ type: Sequelize.TEXT
+ })
+ })
+ },
+
+ down: function (queryInterface, Sequelize) {
+ return queryInterface.changeColumn('Users', 'accessToken', {
+ type: Sequelize.STRING
+ }).then(function () {
+ return queryInterface.changeColumn('Users', 'refreshToken', {
+ type: Sequelize.STRING
+ })
+ })
+ }
+}
diff --git a/lib/models/user.js b/lib/models/user.js
index 019aab7e..5dd13869 100644
--- a/lib/models/user.js
+++ b/lib/models/user.js
@@ -26,10 +26,10 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT
},
accessToken: {
- type: DataTypes.STRING
+ type: DataTypes.TEXT
},
refreshToken: {
- type: DataTypes.STRING
+ type: DataTypes.TEXT
},
deleteToken: {
type: DataTypes.UUID,
diff --git a/lib/response.js b/lib/response.js
index b1b89c78..4cfa9a74 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -18,7 +18,13 @@ var utils = require('./utils')
// public
var response = {
errorForbidden: function (res) {
- responseError(res, '403', 'Forbidden', 'oh no.')
+ const {req} = res
+ if (req.user) {
+ responseError(res, '403', 'Forbidden', 'oh no.')
+ } else {
+ req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
+ res.redirect(config.serverURL)
+ }
},
errorNotFound: function (res) {
responseError(res, '404', 'Not Found', 'oops.')
@@ -74,6 +80,8 @@ function showIndex (req, res, next) {
ldap: config.isLDAPEnable,
ldapProviderName: config.ldap.providerName,
saml: config.isSAMLEnable,
+ oauth2: config.isOAuth2Enable,
+ oauth2ProviderName: config.oauth2.providerName,
email: config.isEmailEnable,
allowEmailRegister: config.allowEmailRegister,
allowPDFExport: config.allowPDFExport,
@@ -126,7 +134,9 @@ function responseHackMD (res, note) {
google: config.isGoogleEnable,
ldap: config.isLDAPEnable,
ldapProviderName: config.ldap.providerName,
+ oauth2ProviderName: config.oauth2.providerName,
saml: config.isSAMLEnable,
+ oauth2: config.isOAuth2Enable,
email: config.isEmailEnable,
allowEmailRegister: config.allowEmailRegister,
allowPDFExport: config.allowPDFExport
diff --git a/lib/web/auth/index.js b/lib/web/auth/index.js
index eb42fb36..61e7c3f9 100644
--- a/lib/web/auth/index.js
+++ b/lib/web/auth/index.js
@@ -43,6 +43,7 @@ if (config.isDropboxEnable) authRouter.use(require('./dropbox'))
if (config.isGoogleEnable) authRouter.use(require('./google'))
if (config.isLDAPEnable) authRouter.use(require('./ldap'))
if (config.isSAMLEnable) authRouter.use(require('./saml'))
+if (config.isOAuth2Enable) authRouter.use(require('./oauth2'))
if (config.isEmailEnable) authRouter.use(require('./email'))
// logout
diff --git a/lib/web/auth/oauth2/index.js b/lib/web/auth/oauth2/index.js
new file mode 100644
index 00000000..f2a3132d
--- /dev/null
+++ b/lib/web/auth/oauth2/index.js
@@ -0,0 +1,106 @@
+'use strict'
+
+const Router = require('express').Router
+const passport = require('passport')
+const OAuth2Strategy = require('passport-oauth2').Strategy
+const config = require('../../../config')
+const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
+
+let oauth2Auth = module.exports = Router()
+
+class OAuth2CustomStrategy extends OAuth2Strategy {
+ constructor (options, verify) {
+ options.customHeaders = options.customHeaders || {}
+ super(options, verify)
+ this.name = 'oauth2'
+ this._userProfileURL = options.userProfileURL
+ this._oauth2.useAuthorizationHeaderforGET(true)
+ }
+
+ userProfile (accessToken, done) {
+ this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
+ var json
+
+ if (err) {
+ return done(new passport.InternalOAuthError('Failed to fetch user profile', err))
+ }
+
+ try {
+ json = JSON.parse(body)
+ } catch (ex) {
+ return done(new Error('Failed to parse user profile'))
+ }
+
+ let profile = parseProfile(json)
+ profile.provider = 'oauth2'
+
+ done(null, profile)
+ })
+ }
+}
+
+function extractProfileAttribute (data, path) {
+ // can handle stuff like `attrs[0].name`
+ path = path.split('.')
+ for (const segment of path) {
+ const m = segment.match(/([\d\w]+)\[(.*)\]/)
+ data = m ? data[m[1]][m[2]] : data[segment]
+ }
+ return data
+}
+
+function parseProfile (data) {
+ const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
+ const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
+ const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
+
+ return {
+ id: username,
+ username: username,
+ displayName: displayName,
+ email: email
+ }
+}
+
+OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
+ this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
+ var json
+
+ if (err) {
+ return done(new passport.InternalOAuthError('Failed to fetch user profile', err))
+ }
+
+ try {
+ json = JSON.parse(body)
+ } catch (ex) {
+ return done(new Error('Failed to parse user profile'))
+ }
+
+ let profile = parseProfile(json)
+ profile.provider = 'oauth2'
+
+ done(null, profile)
+ })
+}
+
+passport.use(new OAuth2CustomStrategy({
+ authorizationURL: config.oauth2.authorizationURL,
+ tokenURL: config.oauth2.tokenURL,
+ clientID: config.oauth2.clientID,
+ clientSecret: config.oauth2.clientSecret,
+ callbackURL: config.serverURL + '/auth/oauth2/callback',
+ userProfileURL: config.oauth2.userProfileURL
+}, passportGeneralCallback))
+
+oauth2Auth.get('/auth/oauth2', function (req, res, next) {
+ setReturnToFromReferer(req)
+ passport.authenticate('oauth2')(req, res, next)
+})
+
+// github auth callback
+oauth2Auth.get('/auth/oauth2/callback',
+ passport.authenticate('oauth2', {
+ successReturnToOrRedirect: config.serverurl + '/',
+ failureRedirect: config.serverurl + '/'
+ })
+)
diff --git a/lib/web/imageRouter/azure.js b/lib/web/imageRouter/azure.js
new file mode 100644
index 00000000..cc98e5fc
--- /dev/null
+++ b/lib/web/imageRouter/azure.js
@@ -0,0 +1,35 @@
+'use strict'
+const path = require('path')
+
+const config = require('../../config')
+const logger = require('../../logger')
+
+const azure = require('azure-storage')
+
+exports.uploadImage = function (imagePath, callback) {
+ if (!imagePath || typeof imagePath !== 'string') {
+ callback(new Error('Image path is missing or wrong'), null)
+ return
+ }
+
+ if (!callback || typeof callback !== 'function') {
+ logger.error('Callback has to be a function')
+ return
+ }
+
+ var azureBlobService = azure.createBlobService(config.azure.connectionString)
+
+ azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) {
+ if (err) {
+ callback(new Error(err.message), null)
+ } else {
+ azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) {
+ if (err) {
+ callback(new Error(err.message), null)
+ } else {
+ callback(null, azureBlobService.getUrl(config.azure.container, result.name))
+ }
+ })
+ }
+ })
+}
diff --git a/lib/web/imageRouter/filesystem.js b/lib/web/imageRouter/filesystem.js
index da76ba5e..145876a9 100644
--- a/lib/web/imageRouter/filesystem.js
+++ b/lib/web/imageRouter/filesystem.js
@@ -2,6 +2,7 @@
const url = require('url')
const config = require('../../config')
+const logger = require('../../logger')
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {
@@ -10,7 +11,7 @@ exports.uploadImage = function (imagePath, callback) {
}
if (!callback || typeof callback !== 'function') {
- callback(new Error('Callback has to be a function'), null)
+ logger.error('Callback has to be a function')
return
}
diff --git a/lib/web/imageRouter/imgur.js b/lib/web/imageRouter/imgur.js
index 31d5f55c..2a20002c 100644
--- a/lib/web/imageRouter/imgur.js
+++ b/lib/web/imageRouter/imgur.js
@@ -11,7 +11,7 @@ exports.uploadImage = function (imagePath, callback) {
}
if (!callback || typeof callback !== 'function') {
- callback(new Error('Callback has to be a function'), null)
+ logger.error('Callback has to be a function')
return
}
diff --git a/lib/web/imageRouter/minio.js b/lib/web/imageRouter/minio.js
index 099cb926..b921c2d2 100644
--- a/lib/web/imageRouter/minio.js
+++ b/lib/web/imageRouter/minio.js
@@ -4,6 +4,7 @@ const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
+const logger = require('../../logger')
const Minio = require('minio')
const minioClient = new Minio.Client({
@@ -21,7 +22,7 @@ exports.uploadImage = function (imagePath, callback) {
}
if (!callback || typeof callback !== 'function') {
- callback(new Error('Callback has to be a function'), null)
+ logger.error('Callback has to be a function')
return
}
diff --git a/lib/web/imageRouter/s3.js b/lib/web/imageRouter/s3.js
index bcd3ea60..626fe148 100644
--- a/lib/web/imageRouter/s3.js
+++ b/lib/web/imageRouter/s3.js
@@ -4,6 +4,7 @@ const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
+const logger = require('../../logger')
const AWS = require('aws-sdk')
const awsConfig = new AWS.Config(config.s3)
@@ -16,7 +17,7 @@ exports.uploadImage = function (imagePath, callback) {
}
if (!callback || typeof callback !== 'function') {
- callback(new Error('Callback has to be a function'), null)
+ logger.error('Callback has to be a function')
return
}
diff --git a/locales/ko.json b/locales/ko.json
new file mode 100644
index 00000000..84dda60b
--- /dev/null
+++ b/locales/ko.json
@@ -0,0 +1,109 @@
+{
+ "Collaborative markdown notes": "협동 마크다운 노트",
+ "Realtime collaborative markdown notes on all platforms.": "실시간으로 모든 플랫폼에서 마크다운 노트를 함께 작성해보세요.",
+ "Best way to write and share your knowledge in markdown.": "마크다운을 쓰고 공유하는 최고의 플랫폼입니다.",
+ "Intro": "소개",
+ "History": "기록",
+ "New guest note": "새 손님 노트",
+ "Collaborate with URL": "URL을 통한 실시간 협업",
+ "Support charts and MathJax": "차트와 MathJax 지원",
+ "Support slide mode": "슬라이드 모드 지원",
+ "Sign In": "로그인",
+ "Below is the history from browser": "아래는 이 브라우저에서 찾은 기록입니다.",
+ "Welcome!": "환영합니다!",
+ "New note": "새 노트",
+ "or": "또는",
+ "Sign Out": "로그아웃",
+ "Explore all features": "모든 기능 둘러보기",
+ "Select tags...": "태그 선택하기",
+ "Search keyword...": "키워드 검색하기",
+ "Sort by title": "제목 기준 정렬",
+ "Title": "제목",
+ "Sort by time": "시간 기준 정렬",
+ "Time": "시간",
+ "Export history": "기록 내보내기",
+ "Import history": "기록 불러오기",
+ "Clear history": "기록 초기화",
+ "Refresh history": "기록 새로고침",
+ "No history": "기록 없음",
+ "Import from browser": "브라우저에서 불러오기",
+ "Releases": "릴리즈",
+ "Are you sure?": "확실합니까?",
+ "Do you really want to delete this note?": "정말로 이 노트를 삭제하시겠습니까?",
+ "All users will lose their connection.": "모든 사용자의 연결이 끊어집니다.",
+ "Cancel": "취소",
+ "Yes, do it!": "네",
+ "Choose method": "방법 선택",
+ "Sign in via %s": "%s으로 로그인",
+ "New": "새",
+ "Publish": "공개하기",
+ "Extra": "추가",
+ "Revision": "기록",
+ "Slide Mode": "슬라이드 모드",
+ "Export": "내보내기",
+ "Import": "들여오기",
+ "Clipboard": "클립보드",
+ "Download": "다운로드",
+ "Raw HTML": "순수 HTML",
+ "Edit": "수정",
+ "View": "보기",
+ "Both": "한번에",
+ "Help": "도움말",
+ "Upload Image": "이미지 업로드",
+ "Menu": "메뉴",
+ "This page need refresh": "새로고침이 필요합니다",
+ "You have an incompatible client version.": "호환되지 않는 클라이언트입니다.",
+ "Refresh to update.": "새로고침하기",
+ "New version available!": "새로운 버전이 있습니다!",
+ "See releases notes here": "릴리즈 노트를 읽어보세요",
+ "Refresh to enjoy new features.": "새로운 기능을 즐기려면 새로고침하십시오",
+ "Your user state has changed.": "유저 상태가 변경되었습니다.",
+ "Refresh to load new user state.": "새로고침하여 새로운 유저 상태를 적용합니다.",
+ "Refresh": "새로고침",
+ "Contacts": "연락처",
+ "Report an issue": "이슈 보고하기",
+ "Meet us on %s": "%s에서 만나보세요",
+ "Send us email": "이메일 보내기",
+ "Documents": "문서",
+ "Features": "기능",
+ "YAML Metadata": "YAML 속성",
+ "Slide Example": "슬라이드 예제",
+ "Cheatsheet": "치트시트",
+ "Example": "예시",
+ "Syntax": "문법",
+ "Header": "머리글",
+ "Unordered List": "순서 없는 목록",
+ "Ordered List": "순서 있는 목록",
+ "Todo List": "체크리스트",
+ "Blockquote": "인용문",
+ "Bold font": "굵게",
+ "Italics font": "기울임",
+ "Strikethrough": "취소선",
+ "Inserted text": "밑줄",
+ "Marked text": "강조",
+ "Link": "링크",
+ "Image": "이미지",
+ "Code": "코드",
+ "Externals": "Externals",
+ "This is a alert area.": "여기는 알림 공간입니다.",
+ "Revert": "되돌리기",
+ "Import from clipboard": "클립보드에서 불러오기",
+ "Paste your markdown or webpage here...": "마크다운이나 웹페이지 붙여넣기",
+ "Clear": "Clear",
+ "This note is locked": "이 노트는 잠겨있습니다.",
+ "Sorry, only owner can edit this note.": "죄송하지만 소유자만 이 노트를 수정할 수 있습니다.",
+ "OK": "확인",
+ "Reach the limit": "한계에 도달",
+ "Sorry, you've reached the max length this note can be.": "죄송합니다. 노트 최대 길이를 초과하였습니다.",
+ "Please reduce the content or divide it to more notes, thank you!": "노트 길이를 줄여주십시오.",
+ "Import from Gist": "Gist에서 불러오기",
+ "Paste your gist url here...": "Gist URL을 입력하세요",
+ "Import from Snippet": "Import from Snippet",
+ "Select From Available Projects": "가능한 프로젝트 중 선택",
+ "Select From Available Snippets": "Select From Available Snippets",
+ "OR": "또는",
+ "Export to Snippet": "Export to Snippet",
+ "Select Visibility Level": "Select Visibility Level",
+ "Night Theme": "다크 테마",
+ "Follow us on %s and %s.": "%s과 %s에서 저희를 팔로우해보세요"
+}
diff --git a/package.json b/package.json
index f676a4cf..20920e21 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hackmd",
- "version": "1.1.0-ce",
+ "version": "1.1.1-ce",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
"license": "AGPL-3.0",
@@ -20,6 +20,7 @@
"async": "^2.1.4",
"aws-sdk": "^2.7.20",
"base64url": "^3.0.0",
+ "azure-storage": "^2.7.0",
"blueimp-md5": "^2.6.0",
"body-parser": "^1.15.2",
"bootstrap": "^3.3.7",
diff --git a/public/css/slide.css b/public/css/slide.css
index a8591108..f8f9c717 100644
--- a/public/css/slide.css
+++ b/public/css/slide.css
@@ -81,7 +81,8 @@
.task-list-item-checkbox {
font-size: inherit;
height: 1em;
- margin: 0.2em 0 0.2em -0.65em !important;
+ transform: scale(2);
+ margin: 0.15em 0 0.15em -0.84em !important;
}
pre code .wrapper {
diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md
index a6428eed..891c506a 100644
--- a/public/docs/release-notes.md
+++ b/public/docs/release-notes.md
@@ -1,6 +1,27 @@
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
---
diff --git a/public/vendor/md-toc.js b/public/vendor/md-toc.js
index f93f7921..3457d465 100644
--- a/public/vendor/md-toc.js
+++ b/public/vendor/md-toc.js
@@ -54,6 +54,7 @@
var j = i + 1
this._elTitleElement = this.elTitleElements[i]
this._elTitleElementName = this._elTitleElement.tagName
+ this._elTitleElementTitle = this._elTitleElement.textContent.replace(/"/g, '&quot;')
this._elTitleElementText = (typeof this.process === 'function' ? this.process(this._elTitleElement) : this._elTitleElement.innerHTML).replace(/<(?:.|\n)*?>/gm, '')
var id = this._elTitleElement.getAttribute('id')
if (!id) {
@@ -63,7 +64,7 @@
id = '#' + id
}
- this.tocContent += '<li><a href="' + id + '">' + this._elTitleElementText + '</a>'
+ this.tocContent += '<li><a href="' + id + '" title="'+ this._elTitleElementTitle +'">' + this._elTitleElementText + '</a>'
if (j !== this._elTitleElementsLen) {
this._elNextTitleElementName = this.elTitleElements[j].tagName
diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs
index a7c5a0b8..220e0dae 100644
--- a/public/views/index/body.ejs
+++ b/public/views/index/body.ejs
@@ -15,7 +15,7 @@
<% if(allowAnonymous) { %>
<a type="button" href="<%- url %>/new" class="btn btn-sm btn-primary"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
<% } %>
- <% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || email) { %>
+ <% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || oauth2 || email) { %>
<button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
<% } %>
</div>
@@ -51,7 +51,7 @@
<% if (errorMessage && errorMessage.length > 0) { %>
<div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
<% } %>
- <% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || email) { %>
+ <% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || oauth2 || email) { %>
<span class="ui-signin">
<br>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>
@@ -147,6 +147,7 @@
<option value="sv">svenska</option>
<option value="eo">Esperanto</option>
<option value="da">dansk</option>
+ <option value="ko">한국어</option>
</select>
<p>
&copy; 2018 <a href="https://hackmd.io">HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a><% if(privacyStatement) { %> | <a href="<%- url %>/s/privacy" target="_blank"><%= __('Privacy') %></a><% } %><% if(termsOfUse) { %> | <a href="<%- url %>/s/terms-of-use" target="_blank"><%= __('Terms of Use') %></a><% } %>
diff --git a/public/views/shared/signin-modal.ejs b/public/views/shared/signin-modal.ejs
index 82b5cf1f..f0cffad9 100644
--- a/public/views/shared/signin-modal.ejs
+++ b/public/views/shared/signin-modal.ejs
@@ -48,7 +48,12 @@
<i class="fa fa-users"></i> <%= __('Sign in via %s', 'SAML') %>
</a>
<% } %>
- <% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || saml) && ldap) { %>
+ <% if(oauth2) { %>
+ <a href="<%- url %>/auth/oauth2" class="btn btn-lg btn-block btn-social btn-soundcloud">
+ <i class="fa fa-mail-forward"></i> <%= __('Sign in via %s', oauth2ProviderName || 'OAuth2') %>
+ </a>
+ <% } %>
+ <% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || saml || oauth2) && ldap) { %>
<hr>
<% }%>
<% if(ldap) { %>
@@ -73,7 +78,7 @@
</div>
</form>
<% } %>
- <% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap) && email) { %>
+ <% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || oauth2) && email) { %>
<hr>
<% }%>
<% if(email) { %>
diff --git a/yarn.lock b/yarn.lock
index a2c87d2c..e6a9c701 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -85,6 +85,15 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
+ajv@^5.1.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -338,10 +347,34 @@ aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+aws4@^1.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
+
+azure-storage@^2.7.0:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.8.3.tgz#6ee1539188ee42b91c62dad691b39ac5452e142b"
+ dependencies:
+ browserify-mime "~1.2.9"
+ extend "~1.2.1"
+ json-edm-parser "0.1.2"
+ md5.js "1.3.4"
+ readable-stream "~2.0.0"
+ request "^2.86.0"
+ underscore "~1.8.3"
+ uuid "^3.0.0"
+ validator "~9.4.1"
+ xml2js "0.2.8"
+ xmlbuilder "0.4.3"
+
babel-cli@^6.18.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1"
@@ -969,6 +1002,10 @@ browserify-aes@0.4.0:
dependencies:
inherits "^2.0.1"
+browserify-mime@~1.2.9:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f"
+
browserify-zlib@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
@@ -1342,6 +1379,12 @@ colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+combined-stream@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
+ dependencies:
+ delayed-stream "~1.0.0"
+
combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
@@ -2432,10 +2475,14 @@ extend@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
-extend@^3.0.0, extend@~3.0.0:
+extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+extend@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c"
+
extendr@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/extendr/-/extendr-2.1.0.tgz#301aa0bbea565f4d2dc8f570f2a22611a8527b56"
@@ -2490,6 +2537,14 @@ fancy-log@^1.1.0:
chalk "^1.1.1"
time-stamp "^1.0.0"
+fast-deep-equal@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -2694,6 +2749,14 @@ form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
+form-data@~2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "1.0.6"
+ mime-types "^2.1.12"
+
formidable@^1.0.17:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
@@ -3096,6 +3159,10 @@ har-schema@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
har-validator@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
@@ -3103,6 +3170,13 @@ har-validator@~4.2.1:
ajv "^4.9.1"
har-schema "^1.0.5"
+har-validator@~5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+ dependencies:
+ ajv "^5.1.0"
+ har-schema "^2.0.0"
+
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -3147,6 +3221,13 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
hasha@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
@@ -3317,6 +3398,14 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
https-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
@@ -3864,10 +3953,20 @@ jshint@~2.9.4:
shelljs "0.3.x"
strip-json-comments "1.0.x"
+json-edm-parser@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/json-edm-parser/-/json-edm-parser-0.1.2.tgz#1e60b0fef1bc0af67bc0d146dfdde5486cd615b4"
+ dependencies:
+ jsonparse "~1.2.0"
+
json-loader@^0.5.4:
version "0.5.7"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -3913,6 +4012,10 @@ jsonlint@^1.6.2:
JSV ">= 4.0.x"
nomnom ">= 1.5.x"
+jsonparse@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd"
+
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -4460,6 +4563,13 @@ maxmin@^1.1.0:
gzip-size "^1.0.0"
pretty-bytes "^1.0.0"
+md5.js@1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@@ -4580,12 +4690,22 @@ micromatch@^2.1.5, micromatch@^2.3.7:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+
mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.3, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
dependencies:
mime-db "~1.30.0"
+mime-types@~2.1.17:
+ version "2.1.18"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ dependencies:
+ mime-db "~1.33.0"
+
mime@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -4905,7 +5025,7 @@ number-is-nan@^1.0.0:
version "1.3.9"
resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.3.9.tgz#8bab486ff7fa3dfd086656bbe8b17116d3692d2a"
-oauth-sign@~0.8.1:
+oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
@@ -5326,6 +5446,10 @@ performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
pg-connection-string@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
@@ -5809,6 +5933,10 @@ qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+qs@~6.5.1:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@@ -5946,6 +6074,17 @@ readable-stream@2.3.3, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
+readable-stream@~2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
readdirp@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
@@ -6169,6 +6308,31 @@ request@2.81.0, request@2.x, request@^2.40.0, request@^2.72.0, request@^2.79.0,
tunnel-agent "^0.6.0"
uuid "^3.0.0"
+request@^2.86.0:
+ version "2.87.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -6289,6 +6453,10 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+safe-buffer@^5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
safe-json-stringify@~1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
@@ -6299,6 +6467,10 @@ safefs@^3.1.2:
dependencies:
graceful-fs "*"
+sax@0.5.x:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1"
+
sax@1.2.1, sax@>=0.6.0, sax@^1.2.1, sax@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
@@ -7020,6 +7192,12 @@ tough-cookie@^2.3.2, tough-cookie@~2.3.0:
dependencies:
punycode "^1.4.1"
+tough-cookie@~2.3.3:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+ dependencies:
+ punycode "^1.4.1"
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -7345,6 +7523,10 @@ validator@^6.2.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-6.3.0.tgz#47ce23ed8d4eaddfa9d4b8ef0071b6cf1078d7c8"
+validator@~9.4.1:
+ version "9.4.1"
+ resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
+
vary@~1.1.1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -7626,6 +7808,12 @@ xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
+xml2js@0.2.8:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.8.tgz#9b81690931631ff09d1957549faf54f4f980b3c2"
+ dependencies:
+ sax "0.5.x"
+
xml2js@0.4.17:
version "0.4.17"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868"
@@ -7644,6 +7832,10 @@ xml@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
+xmlbuilder@0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58"
+
xmlbuilder@4.2.1, xmlbuilder@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5"