From 02e99277146d8bd912f2f19af1d3e94a6181d90d Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Tue, 13 Dec 2016 22:31:35 +0100
Subject: [PATCH 01/34] Initial support for LDAP server authentication

Limitations as of this commit:

- tlsOptions can only be specified in config.json, not as env vars
- authentication failures are not yet gracefully handled by the UI
  - instead the error message is shown on a blank page (/auth/ldap)
- no email address is associated with the LDAP user's account
- no picture/profile URL is associated with the LDAP user's account
- we might have to generate our own access + refresh tokens,
  because we aren't using oauth. The currently generated
  tokens are just a placeholder.
- 'LDAP Sign in' needs to be translated to each locale
---
 README.md                          |  9 +++++++-
 app.js                             |  6 +++++
 config.json.example                | 12 ++++++++++
 lib/auth.js                        | 33 +++++++++++++++++++++++++++-
 lib/config.js                      | 26 ++++++++++++++++++++++
 lib/response.js                    |  2 ++
 locales/en.json                    |  3 ++-
 package.json                       |  1 +
 public/views/index.ejs             |  5 +++--
 public/views/signin-ldap-modal.ejs | 35 ++++++++++++++++++++++++++++++
 public/views/signin-modal.ejs      | 10 +++++++--
 11 files changed, 135 insertions(+), 7 deletions(-)
 create mode 100644 public/views/signin-ldap-modal.ejs

diff --git a/README.md b/README.md
index bdf97ee28..442cbd5cc 100644
--- a/README.md
+++ b/README.md
@@ -131,6 +131,13 @@ Environment variables (will overwrite other server configs)
 | HMD_DROPBOX_CLIENTSECRET | no example | Dropbox API client secret |
 | HMD_GOOGLE_CLIENTID | no example | Google API client id |
 | HMD_GOOGLE_CLIENTSECRET | no example | Google API client secret |
+| HMD_LDAP_URL | ldap://example.com | url of LDAP server |
+| HMD_LDAP_BINDDN | no example | bindDn for LDAP access |
+| HMD_LDAP_BINDCREDENTIALS | no example | bindCredentials for LDAP access |
+| HMD_LDAP_TOKENSECRET | supersecretkey | secret used for generating access/refresh tokens |
+| HMD_LDAP_SEARCHBASE | o=users,dc=example,dc=com | LDAP directory to begin search from |
+| HMD_LDAP_SEARCHFILTER | (uid={{username}}) | LDAP filter to search with |
+| HMD_LDAP_SEARCHATTRIBUTES | no example | LDAP attributes to search with |
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
 | HMD_EMAIL | `true` or `false` | set to allow email register and signin |
 | HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
@@ -182,7 +189,7 @@ Third-party integration api key settings
 
 | service | settings location | description |
 | ------- | --------- | ----------- |
-| facebook, twitter, github, gitlab, dropbox, google | environment variables or `config.json` | for signin |
+| facebook, twitter, github, gitlab, dropbox, google, ldap | environment variables or `config.json` | for signin |
 | imgur | environment variables or `config.json` | for image upload |
 | google drive, dropbox | `public/js/config.js` | for export and import |
 
diff --git a/app.js b/app.js
index 0d78a153e..440549617 100644
--- a/app.js
+++ b/app.js
@@ -380,6 +380,12 @@ if (config.google) {
             failureRedirect: config.serverurl + '/'
         }));
 }
+// ldap auth
+if (config.ldap) {
+    app.post('/auth/ldap', urlencodedParser,
+        passport.authenticate('ldapauth', { successRedirect: '/' })
+    );
+}
 // email auth
 if (config.email) {
     app.post('/register', urlencodedParser, function (req, res, next) {
diff --git a/config.json.example b/config.json.example
index 22fd5c920..642df4f62 100644
--- a/config.json.example
+++ b/config.json.example
@@ -45,6 +45,18 @@
             "clientID": "change this",
             "clientSecret": "change this"
         },
+        "ldap": {
+            "url": "ldap://change_this",
+            "bindDn": null,
+            "bindCredentials": null,
+            "tokenSecret": "change this",
+            "searchBase": "change this",
+            "searchFilter": "change this",
+            "searchAttributes": "change this",
+            "tlsOptions": {
+                "changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback"
+            }
+        },
         "imgur": {
             "clientID": "change this"
         }
diff --git a/lib/auth.js b/lib/auth.js
index f167cedea..1e21eb9fd 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -7,6 +7,7 @@ var GithubStrategy = require('passport-github').Strategy;
 var GitlabStrategy = require('passport-gitlab2').Strategy;
 var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
 var GoogleStrategy = require('passport-google-oauth20').Strategy;
+var LdapStrategy = require('passport-ldapauth');
 var LocalStrategy = require('passport-local').Strategy;
 var validator = require('validator');
 
@@ -110,6 +111,36 @@ if (config.google) {
         callbackURL: config.serverurl + '/auth/google/callback'
     }, callback));
 }
+// ldap
+if (config.ldap) {
+    passport.use(new LdapStrategy({
+        server: {
+            url: config.ldap.url || null,
+            bindDn: config.ldap.bindDn || null,
+            bindCredentials: config.ldap.bindCredentials || null,
+            searchBase: config.ldap.searchBase || null,
+            searchFilter: config.ldap.searchFilter || null,
+            searchAttributes: config.ldap.searchAttributes || null,
+            tlsOptions: config.ldap.tlsOptions || null
+        },
+    },
+    function(user, done) {
+        var profile = {
+            id: 'LDAP-' + user.uidNumber,
+            username: user.uid,
+            displayName: user.displayName,
+            emails: [],
+            avatarUrl: null,
+            profileUrl: null,
+            provider: 'ldap',
+        }
+        var stringifiedProfile = JSON.stringify(profile);
+        // TODO: Generate secure tokens for LDAP users
+        var accessToken = 'debug-access-token|LDAP-' + user.uidNumber + '|' + config.ldap.tokenSecret + '|' + new Date().getTime();
+        var refreshToken = 'debug-refresh-token|LDAP-' + user.uidNumber + '|' + config.ldap.tokenSecret + '|' + new Date().getTime();
+        callback(accessToken, refreshToken, profile, done);
+    }));
+}
 // email
 if (config.email) {
     passport.use(new LocalStrategy({
@@ -130,4 +161,4 @@ if (config.email) {
             return done(err);
         });
     }));
-}
\ No newline at end of file
+}
diff --git a/lib/config.js b/lib/config.js
index 669fcaa8e..a44c279b7 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -93,6 +93,31 @@ var google = (process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSE
     clientID: process.env.HMD_GOOGLE_CLIENTID,
     clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET
 } : config.google || false;
+var ldap = config.ldap || (
+    process.env.HMD_LDAP_URL ||
+    process.env.HMD_LDAP_BINDDN ||
+    process.env.HMD_LDAP_BINDCREDENTIALS ||
+    process.env.HMD_LDAP_TOKENSECRET ||
+    process.env.HMD_LDAP_SEARCHBASE ||
+    process.env.HMD_LDAP_SEARCHFILTER ||
+    process.env.HMD_LDAP_SEARCHATTRIBUTES
+) || false;
+if (ldap == true)
+    ldap = {};
+if (process.env.HMD_LDAP_URL)
+    ldap.url = process.env.HMD_LDAP_URL;
+if (process.env.HMD_LDAP_BINDDN)
+    ldap.bindDn = process.env.HMD_LDAP_BINDDN;
+if (process.env.HMD_LDAP_BINDCREDENTIALS)
+    ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS;
+if (process.env.HMD_LDAP_TOKENSECRET)
+    ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET;
+if (process.env.HMD_LDAP_SEARCHBASE)
+    ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE;
+if (process.env.HMD_LDAP_SEARCHFILTER)
+    ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER;
+if (process.env.HMD_LDAP_SEARCHATTRIBUTES)
+    ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES;
 var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
 var email = process.env.HMD_EMAIL || config.email || false;
 
@@ -151,6 +176,7 @@ module.exports = {
     gitlab: gitlab,
     dropbox: dropbox,
     google: google,
+    ldap: ldap,
     imgur: imgur,
     email: email,
     imageUploadType: imageUploadType,
diff --git a/lib/response.js b/lib/response.js
index aae398511..f0f491810 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -66,6 +66,7 @@ function showIndex(req, res, next) {
         gitlab: config.gitlab,
         dropbox: config.dropbox,
         google: config.google,
+        ldap: config.ldap,
         email: config.email,
         signin: req.isAuthenticated(),
         infoMessage: req.flash('info'),
@@ -98,6 +99,7 @@ function responseHackMD(res, note) {
         gitlab: config.gitlab,
         dropbox: config.dropbox,
         google: config.google,
+        ldap: config.ldap,
         email: config.email
     });
 }
diff --git a/locales/en.json b/locales/en.json
index f1f0d1408..dda317f17 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -100,5 +100,6 @@
 	"Select From Available Snippets": "Select From Available Snippets",
 	"OR": "OR",
 	"Export to Snippet": "Export to Snippet",
-	"Select Visibility Level": "Select Visibility Level"
+	"Select Visibility Level": "Select Visibility Level",
+	"LDAP Sign in": "LDAP Sign in"
 }
\ No newline at end of file
diff --git a/package.json b/package.json
index 5203e54bd..4ef103120 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
     "passport-github": "^1.1.0",
     "passport-gitlab2": "^2.2.0",
     "passport-google-oauth20": "^1.0.0",
+    "passport-ldapauth": "^0.6.0",
     "passport-local": "^1.0.0",
     "passport-twitter": "^1.0.4",
     "passport.socketio": "^3.6.2",
diff --git a/public/views/index.ejs b/public/views/index.ejs
index 2bec7de05..baca14172 100644
--- a/public/views/index.ejs
+++ b/public/views/index.ejs
@@ -57,7 +57,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 || dropbox || google || email) { %>
+                        <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || 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: 170px;"><%= __('Sign In') %></a>
@@ -93,7 +93,7 @@
                 </div>
 
                 <div id="history" class="section"<% if(!signin) { %> style="display:none;"<% } %>>
-                    <% if(facebook || twitter || github || gitlab || dropbox || google || email) { %>
+                    <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || email) { %>
                     <div class="ui-signin">
                         <p><%= __('Below is the history from browser') %></p>
                     </div>
@@ -192,6 +192,7 @@
         </div>
     </div>
     <%- include signin-modal %>
+    <%- include signin-ldap-modal %>
 
     <% if(useCDN) { %>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
diff --git a/public/views/signin-ldap-modal.ejs b/public/views/signin-ldap-modal.ejs
new file mode 100644
index 000000000..6a665f174
--- /dev/null
+++ b/public/views/signin-ldap-modal.ejs
@@ -0,0 +1,35 @@
+<!-- signin ldap modal -->
+<div class="modal fade signin-ldap-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+                </button>
+                <h4 class="modal-title" id="mySmallModalLabel"><%= __('LDAP Sign in') %></h4>
+            </div>
+            <div class="modal-body" style="text-align: center;">
+                <% if(ldap) { %>
+                <form data-toggle="validator" role="form" class="form-horizontal" method="post" enctype="application/x-www-form-urlencoded">
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <input type="username" class="form-control" name="username" placeholder="Username" required>
+                            <span class="help-block control-label with-errors" style="display: inline;"></span>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <input type="password" class="form-control" name="password" placeholder="Password" required>
+                            <span class="help-block control-label with_errors" style="display: inline;"></span>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <button type="submit" class="btn btn-primary" formaction="<%- url %>/auth/ldap">Sign in</button>
+                        </div>
+                    </div>
+                </form>
+                <% } %>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/public/views/signin-modal.ejs b/public/views/signin-modal.ejs
index acbad256b..ba6c57ff1 100644
--- a/public/views/signin-modal.ejs
+++ b/public/views/signin-modal.ejs
@@ -38,7 +38,13 @@
                     <i class="fa fa-google"></i> <%= __('Sign in via %s', 'Google') %>
                 </a>
                 <% } %>
-                <% if((facebook || twitter || github || gitlab || dropbox || google) && email) { %>
+                <% if(ldap) { %>
+                <a type="button" class="btn btn-lg btn-block btn-social btn-reddit "data-toggle="modal" data-target=".signin-ldap-modal">
+                    <i class="fa fa-book"></i> <%= __('Sign in via %s', 'LDAP') %>
+                </a>
+                <% } %>
+
+                <% if((facebook || twitter || github || gitlab || dropbox || google || ldap) && email) { %>
                 <hr>
                 <% }%>
                 <% if(email) { %>
@@ -67,4 +73,4 @@
             </div>
         </div>
     </div>
-</div>
\ No newline at end of file
+</div>

From 6ba9a2f039fe9c4d7495d30ae4f255b96d7f7530 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Wed, 14 Dec 2016 11:49:33 +0100
Subject: [PATCH 02/34] Added HMD_LDAP_TLS_CA env variable

---
 README.md     | 1 +
 lib/config.js | 6 ++++++
 2 files changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 442cbd5cc..30313fe39 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,7 @@ Environment variables (will overwrite other server configs)
 | HMD_LDAP_SEARCHBASE | o=users,dc=example,dc=com | LDAP directory to begin search from |
 | HMD_LDAP_SEARCHFILTER | (uid={{username}}) | LDAP filter to search with |
 | HMD_LDAP_SEARCHATTRIBUTES | no example | LDAP attributes to search with |
+| HMD_LDAP_TLS_CA | no example | Root CA for LDAP TLS in PEM format |
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
 | HMD_EMAIL | `true` or `false` | set to allow email register and signin |
 | HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
diff --git a/lib/config.js b/lib/config.js
index a44c279b7..053d083bb 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -118,6 +118,12 @@ if (process.env.HMD_LDAP_SEARCHFILTER)
     ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER;
 if (process.env.HMD_LDAP_SEARCHATTRIBUTES)
     ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES;
+if (process.env.HMD_LDAP_TLS_CA) {
+    var ca = {
+        ca: process.env.HMD_LDAP_TLS_CA
+    }
+    ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca
+}
 var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
 var email = process.env.HMD_EMAIL || config.email || false;
 

From 30071637998097b25a36b69af3a1affe3c18bf23 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Wed, 14 Dec 2016 11:50:10 +0100
Subject: [PATCH 03/34] Tokens not required for ldap auth

---
 lib/auth.js | 34 ++++++++++++++++++++++++++++++----
 1 file changed, 30 insertions(+), 4 deletions(-)

diff --git a/lib/auth.js b/lib/auth.js
index 1e21eb9fd..b2c787b98 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -135,10 +135,36 @@ if (config.ldap) {
             provider: 'ldap',
         }
         var stringifiedProfile = JSON.stringify(profile);
-        // TODO: Generate secure tokens for LDAP users
-        var accessToken = 'debug-access-token|LDAP-' + user.uidNumber + '|' + config.ldap.tokenSecret + '|' + new Date().getTime();
-        var refreshToken = 'debug-refresh-token|LDAP-' + user.uidNumber + '|' + config.ldap.tokenSecret + '|' + new Date().getTime();
-        callback(accessToken, refreshToken, profile, done);
+        models.User.findOrCreate({
+            where: {
+                profileid: profile.id.toString()
+            },
+            defaults: {
+                profile: stringifiedProfile,
+            }
+        }).spread(function (user, created) {
+            if (user) {
+                var needSave = false;
+                if (user.profile != stringifiedProfile) {
+                    user.profile = stringifiedProfile;
+                    needSave = true;
+                }
+                if (needSave) {
+                    user.save().then(function () {
+                        if (config.debug)
+                            logger.info('user login: ' + user.id);
+                        return done(null, user);
+                    });
+                } else {
+                    if (config.debug)
+                        logger.info('user login: ' + user.id);
+                    return done(null, user);
+                }
+            }
+        }).catch(function (err) {
+            logger.error('ldap auth failed: ' + err);
+            return done(err, null);
+        });
     }));
 }
 // email

From fc8d709afb8a0ff78f649c9ec3b405a68b56a3c0 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Wed, 14 Dec 2016 12:40:54 +0100
Subject: [PATCH 04/34] LDAP login improvements

- return bad request if no username or password given
- return to referer url on auth success
- flash error message on auth failure
---
 app.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/app.js b/app.js
index 440549617..e3ce93de6 100644
--- a/app.js
+++ b/app.js
@@ -382,9 +382,15 @@ if (config.google) {
 }
 // ldap auth
 if (config.ldap) {
-    app.post('/auth/ldap', urlencodedParser,
-        passport.authenticate('ldapauth', { successRedirect: '/' })
-    );
+    app.post('/auth/ldap', urlencodedParser, function (req, res, next) {
+        if (!req.body.username || !req.body.password) return response.errorBadRequest(res);
+        setReturnToFromReferer(req);
+        passport.authenticate('ldapauth', {
+            successReturnToOrRedirect: config.serverurl + '/',
+            failureRedirect: config.serverurl + '/',
+            failureFlash: true
+        })(req, res, next);
+    });
 }
 // email auth
 if (config.email) {

From 72a0e90f7d09d8a4e06a2629dcb9404eb37c64a0 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Wed, 14 Dec 2016 12:42:42 +0100
Subject: [PATCH 05/34] LDAP signin form moved to main signin-modal

- previously was a separate modal
- now is located on main modal, like email auth
---
 locales/en.json                    |  3 +--
 public/views/index.ejs             |  1 -
 public/views/signin-ldap-modal.ejs | 35 ------------------------------
 public/views/signin-modal.ejs      | 27 +++++++++++++++++++----
 4 files changed, 24 insertions(+), 42 deletions(-)
 delete mode 100644 public/views/signin-ldap-modal.ejs

diff --git a/locales/en.json b/locales/en.json
index dda317f17..f1f0d1408 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -100,6 +100,5 @@
 	"Select From Available Snippets": "Select From Available Snippets",
 	"OR": "OR",
 	"Export to Snippet": "Export to Snippet",
-	"Select Visibility Level": "Select Visibility Level",
-	"LDAP Sign in": "LDAP Sign in"
+	"Select Visibility Level": "Select Visibility Level"
 }
\ No newline at end of file
diff --git a/public/views/index.ejs b/public/views/index.ejs
index baca14172..39674b028 100644
--- a/public/views/index.ejs
+++ b/public/views/index.ejs
@@ -192,7 +192,6 @@
         </div>
     </div>
     <%- include signin-modal %>
-    <%- include signin-ldap-modal %>
 
     <% if(useCDN) { %>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
diff --git a/public/views/signin-ldap-modal.ejs b/public/views/signin-ldap-modal.ejs
deleted file mode 100644
index 6a665f174..000000000
--- a/public/views/signin-ldap-modal.ejs
+++ /dev/null
@@ -1,35 +0,0 @@
-<!-- signin ldap modal -->
-<div class="modal fade signin-ldap-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
-    <div class="modal-dialog modal-sm">
-        <div class="modal-content">
-            <div class="modal-header">
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
-                </button>
-                <h4 class="modal-title" id="mySmallModalLabel"><%= __('LDAP Sign in') %></h4>
-            </div>
-            <div class="modal-body" style="text-align: center;">
-                <% if(ldap) { %>
-                <form data-toggle="validator" role="form" class="form-horizontal" method="post" enctype="application/x-www-form-urlencoded">
-                    <div class="form-group">
-                        <div class="col-sm-12">
-                            <input type="username" class="form-control" name="username" placeholder="Username" required>
-                            <span class="help-block control-label with-errors" style="display: inline;"></span>
-                        </div>
-                    </div>
-                    <div class="form-group">
-                        <div class="col-sm-12">
-                            <input type="password" class="form-control" name="password" placeholder="Password" required>
-                            <span class="help-block control-label with_errors" style="display: inline;"></span>
-                        </div>
-                    </div>
-                    <div class="form-group">
-                        <div class="col-sm-12">
-                            <button type="submit" class="btn btn-primary" formaction="<%- url %>/auth/ldap">Sign in</button>
-                        </div>
-                    </div>
-                </form>
-                <% } %>
-            </div>
-        </div>
-    </div>
-</div>
diff --git a/public/views/signin-modal.ejs b/public/views/signin-modal.ejs
index ba6c57ff1..e71b09c64 100644
--- a/public/views/signin-modal.ejs
+++ b/public/views/signin-modal.ejs
@@ -38,12 +38,31 @@
                     <i class="fa fa-google"></i> <%= __('Sign in via %s', 'Google') %>
                 </a>
                 <% } %>
+                <% if((facebook || twitter || github || gitlab || dropbox || google) && ldap) { %>
+                <hr>
+                <% }%>
                 <% if(ldap) { %>
-                <a type="button" class="btn btn-lg btn-block btn-social btn-reddit "data-toggle="modal" data-target=".signin-ldap-modal">
-                    <i class="fa fa-book"></i> <%= __('Sign in via %s', 'LDAP') %>
-                </a>
+                <h4>Via LDAP</h4>
+                <form data-toggle="validator" role="form" class="form-horizontal" method="post" enctype="application/x-www-form-urlencoded">
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <input type="username" class="form-control" name="username" placeholder="Username" required>
+                            <span class="help-block control-label with-errors" style="display: inline;"></span>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <input type="password" class="form-control" name="password" placeholder="Password" required>
+                            <span class="help-block control-label with_errors" style="display: inline;"></span>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-12">
+                            <button type="submit" class="btn btn-primary" formaction="<%- url %>/auth/ldap">Sign in</button>
+                        </div>
+                    </div>
+                </form>
                 <% } %>
-
                 <% if((facebook || twitter || github || gitlab || dropbox || google || ldap) && email) { %>
                 <hr>
                 <% }%>

From 3491f97f7e18a811e516431fa55429f015cef8c4 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Wed, 14 Dec 2016 13:24:25 +0100
Subject: [PATCH 06/34] LDAP auth use email if provided

---
 lib/auth.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/auth.js b/lib/auth.js
index b2c787b98..4b14e42ca 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -129,7 +129,7 @@ if (config.ldap) {
             id: 'LDAP-' + user.uidNumber,
             username: user.uid,
             displayName: user.displayName,
-            emails: [],
+            emails: user.mail ? [user.mail] : [],
             avatarUrl: null,
             profileUrl: null,
             provider: 'ldap',

From aaf1ff4b2f5ae7ae3a5e4e4a202422484503f559 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Thu, 5 Jan 2017 22:36:40 +0800
Subject: [PATCH 07/34] Add limit for constrain anonymous view note

---
 lib/config.js   | 2 ++
 lib/response.js | 6 +++---
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/lib/config.js b/lib/config.js
index 53497f1f8..1f14dd60e 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -20,6 +20,7 @@ var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT ===
 var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true);
 
 var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true);
+var allowanonymousView = process.env.HMD_ALLOW_ANONYMOUS_VIEW ? (process.env.HMD_ALLOW_ANONYMOUS_VIEW === 'true') : ((typeof config.allowanonymousView === 'boolean') ? config.allowanonymousView : true);
 
 var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl;
 
@@ -128,6 +129,7 @@ module.exports = {
     serverurl: getserverurl(),
     usecdn: usecdn,
     allowanonymous: allowanonymous,
+    allowanonymousView: allowanonymousView,
     allowfreeurl: allowfreeurl,
     dburl: dburl,
     db: db,
diff --git a/lib/response.js b/lib/response.js
index a0dc8b1f2..698548159 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -117,7 +117,7 @@ function newNote(req, res, next) {
 }
 
 function checkViewPermission(req, note) {
-    if (note.permission == 'private') {
+    if (note.permission == 'private' || !config.allowanonymousView) {
         if (!req.isAuthenticated() || note.ownerId != req.user.id)
             return false;
         else
@@ -161,7 +161,7 @@ function showNote(req, res, next) {
     findNote(req, res, function (note) {
         // force to use note id
         var noteId = req.params.noteId;
-        var id = LZString.compressToBase64(note.id); 
+        var id = LZString.compressToBase64(note.id);
         if ((note.alias && noteId != note.alias) || (!note.alias && noteId != id))
             return res.redirect(config.serverurl + "/" + (note.alias || id));
         return responseHackMD(res, note);
@@ -413,7 +413,7 @@ function publishSlideActions(req, res, next) {
             res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
             break;
         default:
-            res.redirect(config.serverurl + '/p/' + note.shortid);    
+            res.redirect(config.serverurl + '/p/' + note.shortid);
             break;
         }
     });

From 1fbecbb03d8e94d310885da7d26e1654c548c364 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Thu, 5 Jan 2017 23:37:10 +0800
Subject: [PATCH 08/34] Fix anonymouse view permission check

---
 lib/response.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/response.js b/lib/response.js
index 698548159..e8430d604 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -117,12 +117,15 @@ function newNote(req, res, next) {
 }
 
 function checkViewPermission(req, note) {
-    if (note.permission == 'private' || !config.allowanonymousView) {
+    if (note.permission == 'private') {
         if (!req.isAuthenticated() || note.ownerId != req.user.id)
             return false;
         else
             return true;
     } else {
+        if(!config.allowanonymousView && !req.isAuthenticated()) {
+            return false;
+        }
         return true;
     }
 }

From 9a23fec2390101b6ef7333674e8a8a84f0ebb339 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Fri, 6 Jan 2017 00:15:14 +0800
Subject: [PATCH 09/34] Update readme

---
 README.md | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index a2980ee48..29477b9b9 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@ HackMD
 
 [![Join the chat at https://gitter.im/hackmdio/hackmd](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hackmdio/hackmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
-HackMD lets you create realtime collaborative markdown notes on all platforms.  
-Inspired by Hackpad, with more focus on speed and flexibility.  
+HackMD lets you create realtime collaborative markdown notes on all platforms.
+Inspired by Hackpad, with more focus on speed and flexibility.
 Still in the early stage, feel free to fork or contribute to HackMD.
 
 Thanks for using! :smile:
@@ -12,7 +12,7 @@ Thanks for using! :smile:
 [docker-hackmd](https://github.com/hackmdio/docker-hackmd)
 ---
 
-Before you go too far, here is the great docker repo for HackMD.  
+Before you go too far, here is the great docker repo for HackMD.
 With docker, you can deploy a server in minutes without any downtime.
 
 Heroku Deployment
@@ -25,14 +25,14 @@ You can quickly setup a sample heroku hackmd application by clicking the button
 [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.  
+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)
 ---
 
-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.  
+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.
 This tool is also used for official service.
 
 Browsers Requirement
@@ -125,6 +125,7 @@ Environment variables (will overwrite other server configs)
 | HMD_URL_ADDPORT | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set) |
 | HMD_USECDN | `true` or `false` | set to use CDN resources or not (default is `true`) |
 | HMD_ALLOW_ANONYMOUS | `true` or `false` | set to allow anonymous usage (default is `true`) |
+| HMD_ALLOW_ANONYMOUS_VIEW | `true` or `false` | set to allow anonymous view note (default is `true`) |
 | HMD_ALLOW_FREEURL | `true` or `false` | set to allow new note by accessing not exist note url |
 | HMD_DB_URL | `mysql://localhost:3306/database` | set the db url |
 | HMD_FACEBOOK_CLIENTID | no example | Facebook API client id |
@@ -213,9 +214,9 @@ Third-party integration oauth callback urls
 Operational Transformation
 ---
 
-From 0.3.2, we started supporting operational transformation.  
-It makes concurrent editing safe and will not break up other users' operations.  
-Additionally, now can show other clients' selections.  
+From 0.3.2, we started supporting operational transformation.
+It makes concurrent editing safe and will not break up other users' operations.
+Additionally, now can show other clients' selections.
 See more at [http://operational-transformation.github.io/](http://operational-transformation.github.io/)
 
 **License under MIT.**

From 01361afa7a739f05493ca4a718d6ce5ffe728bc4 Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Fri, 6 Jan 2017 05:37:40 +0100
Subject: [PATCH 10/34] Profile pictures for LDAP users

---
 lib/letter-avatars.js | 23 +++++++++++++++++++++++
 lib/models/user.js    | 11 +++++++++++
 package.json          |  1 +
 3 files changed, 35 insertions(+)
 create mode 100644 lib/letter-avatars.js

diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js
new file mode 100644
index 000000000..e4d4f8404
--- /dev/null
+++ b/lib/letter-avatars.js
@@ -0,0 +1,23 @@
+"use strict";
+
+// external modules
+var seedrandom = require('seedrandom');
+
+// core
+module.exports = function(name) {
+    var colors = ["#5A8770", "#B2B7BB", "#6FA9AB", "#F5AF29", "#0088B9", "#F18636", "#D93A37", "#A6B12E", "#5C9BBC", "#F5888D", "#9A89B5", "#407887", "#9A89B5", "#5A8770", "#D33F33", "#A2B01F", "#F0B126", "#0087BF", "#F18636", "#0087BF", "#B2B7BB", "#72ACAE", "#9C8AB4", "#5A8770", "#EEB424", "#407887"];
+    var color = colors[Math.floor(seedrandom(name)() * colors.length)];
+    var letter = name.substring(0, 1).toUpperCase();
+
+    var 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 + '" />';
+    svg += '<text font-size="64px" font-family="sans-serif" text-anchor="middle" fill="#ffffff">';
+    svg += '<tspan x="48" y="72" stroke-width=".26458px" fill="#ffffff">' + letter + '</tspan>';
+    svg += '</text>';
+    svg += '</g>';
+    svg += '</svg>';
+
+    return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64');
+};
diff --git a/lib/models/user.js b/lib/models/user.js
index aaf344dec..7d27242cf 100644
--- a/lib/models/user.js
+++ b/lib/models/user.js
@@ -7,6 +7,7 @@ var scrypt = require('scrypt');
 
 // core
 var logger = require("../logger.js");
+var letterAvatars = require('../letter-avatars.js');
 
 module.exports = function (sequelize, DataTypes) {
     var User = sequelize.define("User", {
@@ -105,6 +106,16 @@ module.exports = function (sequelize, DataTypes) {
                     case "google":
                         photo = profile.photos[0].value.replace(/(\?sz=)\d*$/i, '$196');
                         break;
+                    case "ldap":
+                        //no image api provided,
+                        //use gravatar if email exists,
+                        //otherwise generate a letter avatar
+                        if (profile.emails[0]) {
+                            photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) + '?s=96';
+                        } else {
+                            photo = letterAvatars(profile.username);
+                        }
+                        break;
                 }
                 return photo;
             },
diff --git a/package.json b/package.json
index 4ef103120..e75f901e7 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,7 @@
     "reveal.js": "^3.3.0",
     "sequelize": "^3.24.3",
     "scrypt": "^6.0.3",
+    "seedrandom": "^2.4.2",
     "select2": "^3.5.2-browserify",
     "sequelize-cli": "^2.4.0",
     "shortid": "2.2.6",

From b044c2ae19e1cacf81524dad15739ea61f4479cb Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Fri, 6 Jan 2017 07:06:58 +0100
Subject: [PATCH 11/34] Use randomcolor not seedrandom for avatar backgrounds

---
 lib/letter-avatars.js | 5 ++---
 package.json          | 1 -
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js
index e4d4f8404..437e3e769 100644
--- a/lib/letter-avatars.js
+++ b/lib/letter-avatars.js
@@ -1,12 +1,11 @@
 "use strict";
 
 // external modules
-var seedrandom = require('seedrandom');
+var randomcolor = require('randomcolor');
 
 // core
 module.exports = function(name) {
-    var colors = ["#5A8770", "#B2B7BB", "#6FA9AB", "#F5AF29", "#0088B9", "#F18636", "#D93A37", "#A6B12E", "#5C9BBC", "#F5888D", "#9A89B5", "#407887", "#9A89B5", "#5A8770", "#D33F33", "#A2B01F", "#F0B126", "#0087BF", "#F18636", "#0087BF", "#B2B7BB", "#72ACAE", "#9C8AB4", "#5A8770", "#EEB424", "#407887"];
-    var color = colors[Math.floor(seedrandom(name)() * colors.length)];
+    var color = randomcolor({seed: name});
     var letter = name.substring(0, 1).toUpperCase();
 
     var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
diff --git a/package.json b/package.json
index e75f901e7..4ef103120 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,6 @@
     "reveal.js": "^3.3.0",
     "sequelize": "^3.24.3",
     "scrypt": "^6.0.3",
-    "seedrandom": "^2.4.2",
     "select2": "^3.5.2-browserify",
     "sequelize-cli": "^2.4.0",
     "shortid": "2.2.6",

From e4fe93249f2af24b113573825aa66350b25e405e Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Fri, 6 Jan 2017 07:18:22 +0100
Subject: [PATCH 12/34] dark avatar backgrounds only

---
 lib/letter-avatars.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js
index 437e3e769..767a2efd3 100644
--- a/lib/letter-avatars.js
+++ b/lib/letter-avatars.js
@@ -5,7 +5,10 @@ var randomcolor = require('randomcolor');
 
 // core
 module.exports = function(name) {
-    var color = randomcolor({seed: name});
+    var color = randomcolor({
+        seed: name,
+        luminosity: 'dark',
+    });
     var letter = name.substring(0, 1).toUpperCase();
 
     var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';

From 94abfaba7c5b7655eda3d6547144adfa26c90a5f Mon Sep 17 00:00:00 2001
From: alecdwm <alec@owls.io>
Date: Fri, 6 Jan 2017 07:21:59 +0100
Subject: [PATCH 13/34] removed comma

---
 lib/letter-avatars.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js
index 767a2efd3..3afa03fe1 100644
--- a/lib/letter-avatars.js
+++ b/lib/letter-avatars.js
@@ -7,7 +7,7 @@ var randomcolor = require('randomcolor');
 module.exports = function(name) {
     var color = randomcolor({
         seed: name,
-        luminosity: 'dark',
+        luminosity: 'dark'
     });
     var letter = name.substring(0, 1).toUpperCase();
 

From ff545b268871be7b6552638427a59a9a6eac5dd1 Mon Sep 17 00:00:00 2001
From: neopostmodern <clemens@neopostmodern.com>
Date: Mon, 9 Jan 2017 12:49:23 +0100
Subject: [PATCH 14/34] Allow displaying LDAP provider name on sign-in modal

---
 README.md                     | 1 +
 lib/config.js                 | 6 +++++-
 public/views/signin-modal.ejs | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 7040aeb34..4717ca7c1 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,7 @@ Environment variables (will overwrite other server configs)
 | HMD_LDAP_SEARCHFILTER | (uid={{username}}) | LDAP filter to search with |
 | HMD_LDAP_SEARCHATTRIBUTES | no example | LDAP attributes to search with |
 | HMD_LDAP_TLS_CA | no example | Root CA for LDAP TLS in PEM format |
+| HMD_LDAP_PROVIDERNAME | My institution | Optional name to be displayed at login form indicating the LDAP provider | 
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
 | HMD_EMAIL | `true` or `false` | set to allow email register and signin |
 | HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
diff --git a/lib/config.js b/lib/config.js
index 2f6792b71..6b2ba0b6c 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -102,7 +102,8 @@ var ldap = config.ldap || (
     process.env.HMD_LDAP_TOKENSECRET ||
     process.env.HMD_LDAP_SEARCHBASE ||
     process.env.HMD_LDAP_SEARCHFILTER ||
-    process.env.HMD_LDAP_SEARCHATTRIBUTES
+    process.env.HMD_LDAP_SEARCHATTRIBUTES ||
+    process.env.HMD_LDAP_PROVIDERNAME
 ) || false;
 if (ldap == true)
     ldap = {};
@@ -126,6 +127,9 @@ if (process.env.HMD_LDAP_TLS_CA) {
     }
     ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca
 }
+if (process.env.HMD_LDAP_PROVIDERNAME) {
+    ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME;
+}
 var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
 var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email;
 
diff --git a/public/views/signin-modal.ejs b/public/views/signin-modal.ejs
index 7c52e0f37..e9c54b33c 100644
--- a/public/views/signin-modal.ejs
+++ b/public/views/signin-modal.ejs
@@ -42,7 +42,7 @@
                 <hr>
                 <% }%>
                 <% if(ldap) { %>
-                <h4>Via LDAP</h4>
+                <h4>Via <% if (ldap.providerName) { %> <%- ldap.providerName %> (LDAP) <% } else { %> LDAP <% } %></h4>
                 <form data-toggle="validator" role="form" class="form-horizontal" method="post" enctype="application/x-www-form-urlencoded">
                     <div class="form-group">
                         <div class="col-sm-12">

From f8e5b547670e3eeb75af3249bb9ad1ee2f44e58f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 09:32:44 +0800
Subject: [PATCH 15/34] Remove temporary change

---
 README.md       | 1 -
 lib/config.js   | 2 --
 lib/response.js | 3 ---
 3 files changed, 6 deletions(-)

diff --git a/README.md b/README.md
index 29477b9b9..6b473b573 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,6 @@ Environment variables (will overwrite other server configs)
 | HMD_URL_ADDPORT | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set) |
 | HMD_USECDN | `true` or `false` | set to use CDN resources or not (default is `true`) |
 | HMD_ALLOW_ANONYMOUS | `true` or `false` | set to allow anonymous usage (default is `true`) |
-| HMD_ALLOW_ANONYMOUS_VIEW | `true` or `false` | set to allow anonymous view note (default is `true`) |
 | HMD_ALLOW_FREEURL | `true` or `false` | set to allow new note by accessing not exist note url |
 | HMD_DB_URL | `mysql://localhost:3306/database` | set the db url |
 | HMD_FACEBOOK_CLIENTID | no example | Facebook API client id |
diff --git a/lib/config.js b/lib/config.js
index 1f14dd60e..53497f1f8 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -20,7 +20,6 @@ var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT ===
 var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true);
 
 var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true);
-var allowanonymousView = process.env.HMD_ALLOW_ANONYMOUS_VIEW ? (process.env.HMD_ALLOW_ANONYMOUS_VIEW === 'true') : ((typeof config.allowanonymousView === 'boolean') ? config.allowanonymousView : true);
 
 var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl;
 
@@ -129,7 +128,6 @@ module.exports = {
     serverurl: getserverurl(),
     usecdn: usecdn,
     allowanonymous: allowanonymous,
-    allowanonymousView: allowanonymousView,
     allowfreeurl: allowfreeurl,
     dburl: dburl,
     db: db,
diff --git a/lib/response.js b/lib/response.js
index e8430d604..910db863d 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -123,9 +123,6 @@ function checkViewPermission(req, note) {
         else
             return true;
     } else {
-        if(!config.allowanonymousView && !req.isAuthenticated()) {
-            return false;
-        }
         return true;
     }
 }

From c21fb8e2a0030095fd8dbfd13f6ba84e933b0e2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 09:35:21 +0800
Subject: [PATCH 16/34] Recovery tariling spaces

---
 README.md       | 18 +++++++++---------
 lib/response.js |  4 ++--
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index 6b473b573..a2980ee48 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@ HackMD
 
 [![Join the chat at https://gitter.im/hackmdio/hackmd](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hackmdio/hackmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
-HackMD lets you create realtime collaborative markdown notes on all platforms.
-Inspired by Hackpad, with more focus on speed and flexibility.
+HackMD lets you create realtime collaborative markdown notes on all platforms.  
+Inspired by Hackpad, with more focus on speed and flexibility.  
 Still in the early stage, feel free to fork or contribute to HackMD.
 
 Thanks for using! :smile:
@@ -12,7 +12,7 @@ Thanks for using! :smile:
 [docker-hackmd](https://github.com/hackmdio/docker-hackmd)
 ---
 
-Before you go too far, here is the great docker repo for HackMD.
+Before you go too far, here is the great docker repo for HackMD.  
 With docker, you can deploy a server in minutes without any downtime.
 
 Heroku Deployment
@@ -25,14 +25,14 @@ You can quickly setup a sample heroku hackmd application by clicking the button
 [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.
+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)
 ---
 
-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.
+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.  
 This tool is also used for official service.
 
 Browsers Requirement
@@ -213,9 +213,9 @@ Third-party integration oauth callback urls
 Operational Transformation
 ---
 
-From 0.3.2, we started supporting operational transformation.
-It makes concurrent editing safe and will not break up other users' operations.
-Additionally, now can show other clients' selections.
+From 0.3.2, we started supporting operational transformation.  
+It makes concurrent editing safe and will not break up other users' operations.  
+Additionally, now can show other clients' selections.  
 See more at [http://operational-transformation.github.io/](http://operational-transformation.github.io/)
 
 **License under MIT.**
diff --git a/lib/response.js b/lib/response.js
index 910db863d..a0dc8b1f2 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -161,7 +161,7 @@ function showNote(req, res, next) {
     findNote(req, res, function (note) {
         // force to use note id
         var noteId = req.params.noteId;
-        var id = LZString.compressToBase64(note.id);
+        var id = LZString.compressToBase64(note.id); 
         if ((note.alias && noteId != note.alias) || (!note.alias && noteId != id))
             return res.redirect(config.serverurl + "/" + (note.alias || id));
         return responseHackMD(res, note);
@@ -413,7 +413,7 @@ function publishSlideActions(req, res, next) {
             res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
             break;
         default:
-            res.redirect(config.serverurl + '/p/' + note.shortid);
+            res.redirect(config.serverurl + '/p/' + note.shortid);    
             break;
         }
     });

From 89b8ddeabae81fd3a8891ce9d8191fbc9e27c83c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 10:02:37 +0800
Subject: [PATCH 17/34] Add limited and protected permission

---
 lib/models/note.js    |  6 +++---
 lib/realtime.js       | 20 ++++++++++----------
 lib/response.js       |  9 +++++++--
 public/js/index.js    | 22 +++++++++++++++++++++-
 public/views/body.ejs |  2 ++
 5 files changed, 43 insertions(+), 16 deletions(-)

diff --git a/lib/models/note.js b/lib/models/note.js
index 132f8b1e1..47d9b97ae 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -23,7 +23,7 @@ var logger = require("../logger.js");
 var ot = require("../ot/index.js");
 
 // permission types
-var permissionTypes = ["freely", "editable", "locked", "private"];
+var permissionTypes = ["freely", "editable", "locked", "private", "limited", "protected"];
 
 module.exports = function (sequelize, DataTypes) {
     var Note = sequelize.define("Note", {
@@ -333,7 +333,7 @@ module.exports = function (sequelize, DataTypes) {
                     if (meta.slideOptions && (typeof meta.slideOptions == "object"))
                         _meta.slideOptions = meta.slideOptions;
                 }
-                return _meta; 
+                return _meta;
             },
             updateAuthorshipByOperation: function (operation, userId, authorships) {
                 var index = 0;
@@ -532,4 +532,4 @@ module.exports = function (sequelize, DataTypes) {
     });
 
     return Note;
-};
\ No newline at end of file
+};
diff --git a/lib/realtime.js b/lib/realtime.js
index a662deeb2..b728622f6 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -251,13 +251,13 @@ function getStatus(callback) {
             return logger.error('count user failed: ' + err);
         });
     }).catch(function (err) {
-        return logger.error('count note failed: ' + err); 
+        return logger.error('count note failed: ' + err);
     });
 }
 
 function isReady() {
-    return realtime.io 
-    && Object.keys(notes).length == 0 && Object.keys(users).length == 0 
+    return realtime.io
+    && Object.keys(notes).length == 0 && Object.keys(users).length == 0
     && connectionSocketQueue.length == 0 && !isConnectionBusy
     && disconnectSocketQueue.length == 0 && !isDisconnectBusy;
 }
@@ -420,7 +420,7 @@ function finishConnection(socket, note, user) {
 function startConnection(socket) {
     if (isConnectionBusy) return;
     isConnectionBusy = true;
-        
+
     var noteId = socket.noteId;
     if (!noteId) {
         return failConnection(404, 'note id not found', socket);
@@ -521,7 +521,7 @@ function disconnect(socket) {
         logger.info("SERVER disconnected a client");
         logger.info(JSON.stringify(users[socket.id]));
     }
-	
+
     if (users[socket.id]) {
         delete users[socket.id];
     }
@@ -618,12 +618,12 @@ function ifMayEdit(socket, callback) {
         case "freely":
             //not blocking anyone
             break;
-        case "editable":
+        case "editable": case: "limited":
             //only login user can change
             if (!socket.request.user || !socket.request.user.logged_in)
                 mayEdit = false;
             break;
-        case "locked": case "private":
+        case "locked": case "private": case "protected":
             //only owner can change
             if (!note.owner || note.owner != socket.request.user.id)
                 mayEdit = false;
@@ -672,7 +672,7 @@ function operationCallback(socket, operation) {
             var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id);
             if (note.server) history.updateHistory(userId, noteId, note.server.document);
         }, 0);
-        
+
     }
     // save authorship
     note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship);
@@ -689,10 +689,10 @@ function connection(socket) {
         }
 
         if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return;
-        
+
         // store noteId in this socket session
         socket.noteId = noteId;
-        
+
         //initialize user data
         //random color
         var color = randomcolor();
diff --git a/lib/response.js b/lib/response.js
index a0dc8b1f2..4438be245 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -122,6 +122,11 @@ function checkViewPermission(req, note) {
             return false;
         else
             return true;
+    } else if (note.permission == 'limited' || note.permission == 'protected') {
+        if( !req.isAuthenticated() ) {
+            return false;
+        }
+        return true;
     } else {
         return true;
     }
@@ -161,7 +166,7 @@ function showNote(req, res, next) {
     findNote(req, res, function (note) {
         // force to use note id
         var noteId = req.params.noteId;
-        var id = LZString.compressToBase64(note.id); 
+        var id = LZString.compressToBase64(note.id);
         if ((note.alias && noteId != note.alias) || (!note.alias && noteId != id))
             return res.redirect(config.serverurl + "/" + (note.alias || id));
         return responseHackMD(res, note);
@@ -413,7 +418,7 @@ function publishSlideActions(req, res, next) {
             res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
             break;
         default:
-            res.redirect(config.serverurl + '/p/' + note.shortid);    
+            res.redirect(config.serverurl + '/p/' + note.shortid);
             break;
         }
     });
diff --git a/public/js/index.js b/public/js/index.js
index 567666574..5b606242f 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -857,7 +857,9 @@ window.ui = {
             freely: $(".ui-permission-freely"),
             editable: $(".ui-permission-editable"),
             locked: $(".ui-permission-locked"),
-            private: $(".ui-permission-private")
+            private: $(".ui-permission-private"),
+            limited: $(".ui-permission-limited"),
+            protected: $(".ui-permission-protected")
         },
         delete: $(".ui-delete-note")
     },
@@ -2247,6 +2249,14 @@ ui.infobar.permission.locked.click(function () {
 ui.infobar.permission.private.click(function () {
     emitPermission("private");
 });
+//limited
+ui.infobar.permission.limited.click(function() {
+    emitPermission("limited");
+});
+//protected
+ui.infobar.permission.protected.click(function() {
+    emitPermission("protected");
+});
 // delete note
 ui.infobar.delete.click(function () {
     $('.delete-modal').modal('show');
@@ -2285,6 +2295,14 @@ function updatePermission(newPermission) {
             label = '<i class="fa fa-hand-stop-o"></i> Private';
             title = "Only owner can view & edit";
             break;
+        case "limited":
+            label = '<i class="fa fa-hand-shield"></i> Limited';
+            title = "Signed people can view and edit"
+            break;
+        case "protected":
+            label = '<i class="fa fa-hand-stop-o"></i> Protected';
+            title = "Signed people can view";
+            break;
     }
     if (personalInfo.userid && owner && personalInfo.userid == owner) {
         label += ' <i class="fa fa-caret-down"></i>';
@@ -2302,6 +2320,7 @@ function havePermission() {
             bool = true;
             break;
         case "editable":
+        case "limited":
             if (!personalInfo.login) {
                 bool = false;
             } else {
@@ -2310,6 +2329,7 @@ function havePermission() {
             break;
         case "locked":
         case "private":
+        case "protected":
             if (!owner || personalInfo.userid != owner) {
                 bool = false;
             } else {
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 83a82fa3a..79e361411 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -19,6 +19,8 @@
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
                         <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
                         <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
+                        <li class="ui-permission-limited"><a><i class="fa fa-shield fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
+                        <li class="ui-permission-protected"><a><i class="fa fa-hand-stop-o fa-fw"></i> Protected - Only owner can edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>
                     </ul>

From be7696170fbfb9a0744a1400709479ac2e4c60e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 10:19:18 +0800
Subject: [PATCH 18/34] Fix syntax when use case

---
 lib/realtime.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/realtime.js b/lib/realtime.js
index b728622f6..a3c56c411 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -618,7 +618,7 @@ function ifMayEdit(socket, callback) {
         case "freely":
             //not blocking anyone
             break;
-        case "editable": case: "limited":
+        case "editable": case "limited":
             //only login user can change
             if (!socket.request.user || !socket.request.user.logged_in)
                 mayEdit = false;

From 7b02c48d93d05e3ced0a030a13d620559710c9cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 14:13:30 +0800
Subject: [PATCH 19/34] Adjust permission order to more clarly

---
 lib/models/note.js    |  2 +-
 public/js/index.js    | 14 +++++++-------
 public/views/body.ejs |  4 ++--
 3 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/lib/models/note.js b/lib/models/note.js
index 47d9b97ae..a6921267a 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -23,7 +23,7 @@ var logger = require("../logger.js");
 var ot = require("../ot/index.js");
 
 // permission types
-var permissionTypes = ["freely", "editable", "locked", "private", "limited", "protected"];
+var permissionTypes = ["freely", "editable", "limited", "private", "protected", "locked"];
 
 module.exports = function (sequelize, DataTypes) {
     var Note = sequelize.define("Note", {
diff --git a/public/js/index.js b/public/js/index.js
index 5b606242f..4cbc6b933 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2287,22 +2287,22 @@ function updatePermission(newPermission) {
             label = '<i class="fa fa-shield"></i> Editable';
             title = "Signed people can edit";
             break;
-        case "locked":
-            label = '<i class="fa fa-lock"></i> Locked';
-            title = "Only owner can edit";
+        case "limited":
+            label = '<i class="fa fa-shield"></i> Limited';
+            title = "Signed people can view & edit"
             break;
         case "private":
             label = '<i class="fa fa-hand-stop-o"></i> Private';
             title = "Only owner can view & edit";
             break;
-        case "limited":
-            label = '<i class="fa fa-hand-shield"></i> Limited';
-            title = "Signed people can view and edit"
-            break;
         case "protected":
             label = '<i class="fa fa-hand-stop-o"></i> Protected';
             title = "Signed people can view";
             break;
+        case "locked":
+            label = '<i class="fa fa-lock"></i> Locked';
+            title = "Only owner can edit";
+            break;
     }
     if (personalInfo.userid && owner && personalInfo.userid == owner) {
         label += ' <i class="fa fa-caret-down"></i>';
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 79e361411..988d4f5bc 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -17,10 +17,10 @@
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
                         <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
-                        <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
-                        <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="ui-permission-limited"><a><i class="fa fa-shield fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
+                        <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="ui-permission-protected"><a><i class="fa fa-hand-stop-o fa-fw"></i> Protected - Only owner can edit</a></li>
+                        <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>
                     </ul>

From 758607d37808b7fee2957bdb33eeda44e02233d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 15:15:12 +0800
Subject: [PATCH 20/34] Add fa-stack style to public/js/index.js

---
 public/js/index.js | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/public/js/index.js b/public/js/index.js
index 4cbc6b933..7ddc696a3 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2288,16 +2288,22 @@ function updatePermission(newPermission) {
             title = "Signed people can edit";
             break;
         case "limited":
-            label = '<i class="fa fa-shield"></i> Limited';
-            title = "Signed people can view & edit"
+            label = '<span class="fa-stack" aria-hidden="true">' +
+                    '<i class="fa fa-shield fa-stack-1x"></i>' +
+                    '<i class="fa fa-circle-thin fa-stack-2x"></i>' +
+                    '</span> Limited';
+            title = "Signed people can edit & guest can't view"
             break;
         case "private":
             label = '<i class="fa fa-hand-stop-o"></i> Private';
             title = "Only owner can view & edit";
             break;
         case "protected":
-            label = '<i class="fa fa-hand-stop-o"></i> Protected';
-            title = "Signed people can view";
+            label = '<span class="fa-stack aria-hidden="true">' +
+                    '<i class="fa fa-hand-stop-o"></i>' +
+                    '<i class="fa fa-circle-thin fa-stack-2x"></i>' +
+                    '</span> Protected';
+            title = "Only owner can edit & guest can't view";
             break;
         case "locked":
             label = '<i class="fa fa-lock"></i> Locked';

From a9fe43ac3a38f036d4f9baa27c50fc9b5fea8c1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 17:22:57 +0800
Subject: [PATCH 21/34] Update limited/protected permission icon

---
 public/js/index.js    | 10 ++--------
 public/views/body.ejs |  4 ++--
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/public/js/index.js b/public/js/index.js
index 7ddc696a3..057ddd9d0 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2288,10 +2288,7 @@ function updatePermission(newPermission) {
             title = "Signed people can edit";
             break;
         case "limited":
-            label = '<span class="fa-stack" aria-hidden="true">' +
-                    '<i class="fa fa-shield fa-stack-1x"></i>' +
-                    '<i class="fa fa-circle-thin fa-stack-2x"></i>' +
-                    '</span> Limited';
+            label = '<i class="fa fa-exclamation-circle"></i> Limited';
             title = "Signed people can edit & guest can't view"
             break;
         case "private":
@@ -2299,10 +2296,7 @@ function updatePermission(newPermission) {
             title = "Only owner can view & edit";
             break;
         case "protected":
-            label = '<span class="fa-stack aria-hidden="true">' +
-                    '<i class="fa fa-hand-stop-o"></i>' +
-                    '<i class="fa fa-circle-thin fa-stack-2x"></i>' +
-                    '</span> Protected';
+            label = '<i class="fa fa-umbrella"></i> Protected';
             title = "Only owner can edit & guest can't view";
             break;
         case "locked":
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 988d4f5bc..6500e607c 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -17,9 +17,9 @@
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
                         <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
-                        <li class="ui-permission-limited"><a><i class="fa fa-shield fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
+                        <li class="ui-permission-limited"><a><i class="fa fa-exclamation-circle fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
                         <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
-                        <li class="ui-permission-protected"><a><i class="fa fa-hand-stop-o fa-fw"></i> Protected - Only owner can edit</a></li>
+                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit</a></li>
                         <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>

From d6be0cf755e31002d6265b706785c06e06a0ae56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=92=BC=E6=99=82=E5=BC=A6=E4=B9=9F?= <elct9620@frost.tw>
Date: Tue, 10 Jan 2017 20:22:06 +0800
Subject: [PATCH 22/34] Update limite icon to fa-id-card

---
 public/js/index.js    | 2 +-
 public/views/body.ejs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/js/index.js b/public/js/index.js
index 057ddd9d0..91dc8f0df 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2288,7 +2288,7 @@ function updatePermission(newPermission) {
             title = "Signed people can edit";
             break;
         case "limited":
-            label = '<i class="fa fa-exclamation-circle"></i> Limited';
+            label = '<i class="fa fa-id-card"></i> Limited';
             title = "Signed people can edit & guest can't view"
             break;
         case "private":
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 6500e607c..86469ce94 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -17,7 +17,7 @@
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
                         <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
-                        <li class="ui-permission-limited"><a><i class="fa fa-exclamation-circle fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
+                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
                         <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit</a></li>
                         <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>

From 42d684329fbd1b139b253d448e444349e891d775 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 17:13:52 +0800
Subject: [PATCH 23/34] Workaround text shadow for font antialias might cause
 cut off in Edge

---
 public/css/site.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/css/site.css b/public/css/site.css
index 3685149bc..d88f8429b 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -3,7 +3,7 @@ body {
     font-smoothing: subpixel-antialiased !important;
     -webkit-font-smoothing: subpixel-antialiased !important;
     -moz-osx-font-smoothing: auto !important;
-    text-shadow: 1px 1px 1.2px rgba(0, 0, 0, 0.004);
+    text-shadow: 0 0 1em transparent, 1px 1px 1.2px rgba(0, 0, 0, 0.004);
     /*text-rendering: optimizeLegibility;*/
     -webkit-overflow-scrolling: touch;
     font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;

From 0432fef26762cfa62338a11b864488a4d525b233 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 17:14:32 +0800
Subject: [PATCH 24/34] Fix history list might check pagination on clear and
 open

---
 public/js/cover.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/public/js/cover.js b/public/js/cover.js
index 2f35bd282..a8d8ecfee 100644
--- a/public/js/cover.js
+++ b/public/js/cover.js
@@ -107,9 +107,11 @@ $(".ui-history").click(function () {
 
 function checkHistoryList() {
     if ($("#history-list").children().length > 0) {
+        $('.pagination').show();
         $(".ui-nohistory").hide();
         $(".ui-import-from-browser").hide();
     } else if ($("#history-list").children().length == 0) {
+        $('.pagination').hide();
         $(".ui-nohistory").slideDown();
         getStorageHistory(function (data) {
             if (data && data.length > 0 && getLoginState() && historyList.items.length == 0) {

From fc788e805e8896f3ae967270148939d37923c516 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 17:17:01 +0800
Subject: [PATCH 25/34] Fix SIGINT checkClean should only log error instead
 throw error

---
 app.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app.js b/app.js
index a17d3a614..47448fe15 100644
--- a/app.js
+++ b/app.js
@@ -639,7 +639,7 @@ process.on('SIGINT', function () {
     var checkCleanTimer = setInterval(function () {
         if (history.isReady() && realtime.isReady()) {
             models.Revision.checkAllNotesRevision(function (err, notes) {
-                if (err) throw new Error(err);
+                if (err) return logger.error(err);
                 if (!notes || notes.length <= 0) {
                     clearInterval(checkCleanTimer);
                     return process.exit(0);

From ffa14cfeefb9afcc6f177a9ecfe710ffb3227242 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 17:17:36 +0800
Subject: [PATCH 26/34] Fix sortOnlineUserList might not check property
 existence before comparsion

---
 public/js/index.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/public/js/index.js b/public/js/index.js
index 91dc8f0df..7effcbd7f 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2946,14 +2946,14 @@ function sortOnlineUserList(list) {
                     else if (usera.idle && !userb.idle)
                         return 1;
                     else {
-                        if (usera.name && usera.name.toLowerCase() < userb.name.toLowerCase()) {
+                        if (usera.name && userb.name && usera.name.toLowerCase() < userb.name.toLowerCase()) {
                             return -1;
-                        } else if (usera.name && usera.name.toLowerCase() > userb.name.toLowerCase()) {
+                        } else if (usera.name && userb.name && usera.name.toLowerCase() > userb.name.toLowerCase()) {
                             return 1;
                         } else {
-                            if (usera.color && usera.color.toLowerCase() < userb.color.toLowerCase())
+                            if (usera.color && userb.color && usera.color.toLowerCase() < userb.color.toLowerCase())
                                 return -1;
-                            else if (usera.color && usera.color.toLowerCase() > userb.color.toLowerCase())
+                            else if (usera.color && userb.color && usera.color.toLowerCase() > userb.color.toLowerCase())
                                 return 1;
                             else
                                 return 0;

From 7e191acbde048b4ce274d1dbe49fb63a627a3def Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 17:18:24 +0800
Subject: [PATCH 27/34] Fix author creation in operationCallback might cause
 unique constraint validation error

---
 lib/realtime.js | 30 +++++++++++++++++++-----------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/lib/realtime.js b/lib/realtime.js
index a3c56c411..213906073 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -652,17 +652,25 @@ function operationCallback(socket, operation) {
         if (!user) return;
         userId = socket.request.user.id;
         if (!note.authors[userId]) {
-            models.Author.create({
-                noteId: noteId,
-                userId: userId,
-                color: user.color
-            }).then(function (author) {
-                note.authors[author.userId] = {
-                    userid: author.userId,
-                    color: author.color,
-                    photo: user.photo,
-                    name: user.name
-                };
+            models.Author.findOrCreate({
+                where: {
+                    noteId: noteId,
+                    userId: userId
+                },
+                defaults: {
+                    noteId: noteId,
+                    userId: userId,
+                    color: user.color
+                }
+            }).spread(function (author, created) {
+                if (author) {
+                    note.authors[author.userId] = {
+                        userid: author.userId,
+                        color: author.color,
+                        photo: user.photo,
+                        name: user.name
+                    };
+                }
             }).catch(function (err) {
                 return logger.error('operation callback failed: ' + err);
             });

From 5f65795e79561f7b787767b7c184f66b65f74c5f Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 19:04:17 +0800
Subject: [PATCH 28/34] Fix permission order and keep wording consistency

---
 lib/models/note.js    |  2 +-
 public/js/index.js    | 12 ++++++------
 public/views/body.ejs |  6 +++---
 3 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/lib/models/note.js b/lib/models/note.js
index a6921267a..86112973b 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -23,7 +23,7 @@ var logger = require("../logger.js");
 var ot = require("../ot/index.js");
 
 // permission types
-var permissionTypes = ["freely", "editable", "limited", "private", "protected", "locked"];
+var permissionTypes = ["freely", "editable", "limited", "locked", "protected", "private"];
 
 module.exports = function (sequelize, DataTypes) {
     var Note = sequelize.define("Note", {
diff --git a/public/js/index.js b/public/js/index.js
index 7effcbd7f..1dfb02e12 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2291,17 +2291,17 @@ function updatePermission(newPermission) {
             label = '<i class="fa fa-id-card"></i> Limited';
             title = "Signed people can edit & guest can't view"
             break;
-        case "private":
-            label = '<i class="fa fa-hand-stop-o"></i> Private';
-            title = "Only owner can view & edit";
+        case "locked":
+            label = '<i class="fa fa-lock"></i> Locked';
+            title = "Only owner can edit";
             break;
         case "protected":
             label = '<i class="fa fa-umbrella"></i> Protected';
             title = "Only owner can edit & guest can't view";
             break;
-        case "locked":
-            label = '<i class="fa fa-lock"></i> Locked';
-            title = "Only owner can edit";
+        case "private":
+            label = '<i class="fa fa-hand-stop-o"></i> Private';
+            title = "Only owner can view & edit";
             break;
     }
     if (personalInfo.userid && owner && personalInfo.userid == owner) {
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 86469ce94..f0444befa 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -17,10 +17,10 @@
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
                         <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
-                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed people can edit &amp; view</a></li>
-                        <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
-                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit</a></li>
+                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed people can edit &amp; guest can't view</a></li>
                         <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
+                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit &amp; guest can't view</a></li>
+                        <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>
                     </ul>

From 747629e549fb5c32e1acf18e24bfc6a7e1cd5b0c Mon Sep 17 00:00:00 2001
From: Sheogorath <sheogorath@shivering-isles.com>
Date: Thu, 12 Jan 2017 04:25:58 +0100
Subject: [PATCH 29/34] Add `allowemailregister` option

---
 README.md                     |  6 ++--
 app.js                        | 52 ++++++++++++++++++-----------------
 lib/config.js                 |  2 ++
 lib/response.js               |  1 +
 public/views/signin-modal.ejs |  2 +-
 5 files changed, 35 insertions(+), 28 deletions(-)

diff --git a/README.md b/README.md
index 4717ca7c1..7d3d45739 100644
--- a/README.md
+++ b/README.md
@@ -150,7 +150,8 @@ Environment variables (will overwrite other server configs)
 | HMD_LDAP_TLS_CA | no example | Root CA for LDAP TLS in PEM format |
 | HMD_LDAP_PROVIDERNAME | My institution | Optional name to be displayed at login form indicating the LDAP provider | 
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
-| HMD_EMAIL | `true` or `false` | set to allow email register and signin |
+| HMD_EMAIL | `true` or `false` | set to allow email signin |
+| HMD_ALLOW_EMAIL_REGISTER | `true` or `false` | set to allow email register |
 | HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
 | HMD_S3_ACCESS_KEY_ID | no example | AWS access key id |
 | HMD_S3_SECRET_ACCESS_KEY | no example | AWS secret key |
@@ -194,7 +195,8 @@ Server settings `config.json`
 | heartbeatinterval | `5000` | socket.io heartbeat interval |
 | heartbeattimeout | `10000` | socket.io heartbeat timeout |
 | documentmaxlength | `100000` | note max length |
-| email | `true` or `false` | set to allow email register and signin |
+| email | `true` or `false` | set to allow email signin |
+| allowemailregister  | `true` or `false` | set to allow email register |
 | imageUploadType | `imgur`(default), `s3` or `filesystem` | Where to upload image
 | s3 | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION", "bucket": "YOUR_S3_BUCKET_NAME" }` | When `imageUploadType` be setted to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
 
diff --git a/app.js b/app.js
index a17d3a614..83b19e230 100644
--- a/app.js
+++ b/app.js
@@ -395,34 +395,36 @@ if (config.ldap) {
 }
 // email auth
 if (config.email) {
-    app.post('/register', urlencodedParser, function (req, res, next) {
-        if (!req.body.email || !req.body.password) return response.errorBadRequest(res);
-        if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res);
-        models.User.findOrCreate({
-            where: {
-                email: req.body.email
-            },
-            defaults: {
-                password: req.body.password
-            }
-        }).spread(function (user, created) {
-            if (user) {
-                if (created) {
-                    if (config.debug) logger.info('user registered: ' + user.id);
-                    req.flash('info', "You've successfully registered, please signin.");
-                } else {
-                    if (config.debug) logger.info('user found: ' + user.id);
-                    req.flash('error', "This email has been used, please try another one.");
+    if (config.allowemailregister)
+        app.post('/register', urlencodedParser, function (req, res, next) {
+            if (!req.body.email || !req.body.password) return response.errorBadRequest(res);
+            if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res);
+            models.User.findOrCreate({
+                where: {
+                    email: req.body.email
+                },
+                defaults: {
+                    password: req.body.password
                 }
+            }).spread(function (user, created) {
+                if (user) {
+                    if (created) {
+                        if (config.debug) logger.info('user registered: ' + user.id);
+                        req.flash('info', "You've successfully registered, please signin.");
+                    } else {
+                        if (config.debug) logger.info('user found: ' + user.id);
+                        req.flash('error', "This email has been used, please try another one.");
+                    }
+                    return res.redirect(config.serverurl + '/');
+                }
+                req.flash('error', "Failed to register your account, please try again.");
                 return res.redirect(config.serverurl + '/');
-            }
-            req.flash('error', "Failed to register your account, please try again.");
-            return res.redirect(config.serverurl + '/');
-        }).catch(function (err) {
-            logger.error('auth callback failed: ' + err);
-            return response.errorInternalError(res);
+            }).catch(function (err) {
+                logger.error('auth callback failed: ' + err);
+                return response.errorInternalError(res);
+            });
         });
-    });
+
     app.post('/login', urlencodedParser, function (req, res, next) {
         if (!req.body.email || !req.body.password) return response.errorBadRequest(res);
         if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res);
diff --git a/lib/config.js b/lib/config.js
index 6b2ba0b6c..031c6741b 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -132,6 +132,7 @@ if (process.env.HMD_LDAP_PROVIDERNAME) {
 }
 var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
 var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email;
+var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_HMD_ALLOW_EMAIL_REGISTER === 'true') : !!config.allowemailregister;
 
 function getserverurl() {
     var url = '';
@@ -194,6 +195,7 @@ module.exports = {
     ldap: ldap,
     imgur: imgur,
     email: email,
+    allowemailregister: allowemailregister,
     imageUploadType: imageUploadType,
     s3: s3,
     s3bucket: s3bucket
diff --git a/lib/response.js b/lib/response.js
index 6c1db9672..9014a0a09 100755
--- a/lib/response.js
+++ b/lib/response.js
@@ -68,6 +68,7 @@ function showIndex(req, res, next) {
         google: config.google,
         ldap: config.ldap,
         email: config.email,
+        allowemailregister: config.allowemailregister,
         signin: req.isAuthenticated(),
         infoMessage: req.flash('info'),
         errorMessage: req.flash('error')
diff --git a/public/views/signin-modal.ejs b/public/views/signin-modal.ejs
index e9c54b33c..a8af62e7d 100644
--- a/public/views/signin-modal.ejs
+++ b/public/views/signin-modal.ejs
@@ -84,7 +84,7 @@
                     <div class="form-group">
                         <div class="col-sm-12">
                             <button type="submit" class="btn btn-primary" formaction="<%- url %>/login">Sign in</button>
-                            <button type="submit" class="btn btn-default" formaction="<%- url %>/register">Register</button>
+                            <% if(allowemailregister) { %><button type="submit" class="btn btn-default" formaction="<%- url %>/register">Register</button><% }%>
                         </div>
                     </div>
                 </form>

From 8b378d7847b82f34ceedfdfbf27d5fe324ab1944 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 23:36:23 +0800
Subject: [PATCH 30/34] Update to use shorter wording in limited and protected
 permissions

---
 public/js/index.js    | 4 ++--
 public/views/body.ejs | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/public/js/index.js b/public/js/index.js
index 1dfb02e12..14235bc3b 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -2289,7 +2289,7 @@ function updatePermission(newPermission) {
             break;
         case "limited":
             label = '<i class="fa fa-id-card"></i> Limited';
-            title = "Signed people can edit & guest can't view"
+            title = "Signed people can edit (forbid guest)"
             break;
         case "locked":
             label = '<i class="fa fa-lock"></i> Locked';
@@ -2297,7 +2297,7 @@ function updatePermission(newPermission) {
             break;
         case "protected":
             label = '<i class="fa fa-umbrella"></i> Protected';
-            title = "Only owner can edit & guest can't view";
+            title = "Only owner can edit (forbid guest)";
             break;
         case "private":
             label = '<i class="fa fa-hand-stop-o"></i> Private';
diff --git a/public/views/body.ejs b/public/views/body.ejs
index f0444befa..5ad1733e3 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -17,9 +17,9 @@
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
                         <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
                         <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed people can edit</a></li>
-                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed people can edit &amp; guest can't view</a></li>
+                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed people can edit (forbid guest)</a></li>
                         <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
-                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit &amp; guest can't view</a></li>
+                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit (forbid guest)</a></li>
                         <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>

From 3ee65cd38e2d3bef114079e971f9a158e2a6d2b2 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 23:45:51 +0800
Subject: [PATCH 31/34] Fix for limited and protected permissions should forbid
 guest in realtime events

---
 lib/realtime.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/realtime.js b/lib/realtime.js
index 213906073..0f2a66801 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -374,7 +374,7 @@ function finishConnection(socket, note, user) {
         return interruptConnection(socket, note, user);
     }
     //check view permission
-    if (note.permission == 'private') {
+    if (note.permission == 'limited' || note.permission == 'protected' || note.permission == 'private') {
         if (socket.request.user && socket.request.user.logged_in && socket.request.user.id == note.owner) {
             //na
         } else {
@@ -790,7 +790,7 @@ function connection(socket) {
                         var sock = note.socks[i];
                         if (typeof sock !== 'undefined' && sock) {
                             //check view permission
-                            if (permission == 'private') {
+                            if (permission == 'limited' || permission == 'protected' || permission == 'private') {
                                 if (sock.request.user && sock.request.user.logged_in && sock.request.user.id == note.owner) {
                                     //na
                                 } else {

From 6be875263a3c0ee849b023983032d8f55e28a2e0 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Thu, 12 Jan 2017 23:53:22 +0800
Subject: [PATCH 32/34] Fix allowemailregister config typo and default should
 be true

---
 lib/config.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/config.js b/lib/config.js
index 031c6741b..f6e7f2c47 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -132,7 +132,7 @@ if (process.env.HMD_LDAP_PROVIDERNAME) {
 }
 var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
 var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email;
-var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_HMD_ALLOW_EMAIL_REGISTER === 'true') : !!config.allowemailregister;
+var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true);
 
 function getserverurl() {
     var url = '';

From 86f0b10775ec85fa3ee0d5c64a814ab34e17f3d6 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Fri, 13 Jan 2017 00:03:29 +0800
Subject: [PATCH 33/34] Fix permission dropdown text might out of range in
 smaller screen

---
 public/css/index.css | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/public/css/index.css b/public/css/index.css
index da1823f2a..8f483aa76 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -240,6 +240,9 @@ body {
 }
 .dropdown-menu > li > a {
     cursor: pointer;
+    text-overflow: ellipsis;
+    max-width: calc(100vw - 30px);
+    overflow: hidden;
 }
 .dropdown-menu.CodeMirror-other-cursor {
     transition: none;

From 3cf40a8dec96af3710a5945dde693fdc949f31af Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han <jacky_cute0808@hotmail.com>
Date: Fri, 13 Jan 2017 00:51:40 +0800
Subject: [PATCH 34/34] Update README.md to describe allowemailregister config
 more clear

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 7d3d45739..2e7b919b5 100644
--- a/README.md
+++ b/README.md
@@ -151,7 +151,7 @@ Environment variables (will overwrite other server configs)
 | HMD_LDAP_PROVIDERNAME | My institution | Optional name to be displayed at login form indicating the LDAP provider | 
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
 | HMD_EMAIL | `true` or `false` | set to allow email signin |
-| HMD_ALLOW_EMAIL_REGISTER | `true` or `false` | set to allow email register |
+| HMD_ALLOW_EMAIL_REGISTER | `true` or `false` | set to allow email register (only applied when email is set, default is `true`) |
 | HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
 | HMD_S3_ACCESS_KEY_ID | no example | AWS access key id |
 | HMD_S3_SECRET_ACCESS_KEY | no example | AWS secret key |
@@ -196,7 +196,7 @@ Server settings `config.json`
 | heartbeattimeout | `10000` | socket.io heartbeat timeout |
 | documentmaxlength | `100000` | note max length |
 | email | `true` or `false` | set to allow email signin |
-| allowemailregister  | `true` or `false` | set to allow email register |
+| allowemailregister  | `true` or `false` | set to allow email register (only applied when email is set, default is `true`) |
 | imageUploadType | `imgur`(default), `s3` or `filesystem` | Where to upload image
 | s3 | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION", "bucket": "YOUR_S3_BUCKET_NAME" }` | When `imageUploadType` be setted to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |