Просмотр исходного кода

Merge branch 'feat/importer' into feat/importer-qiita

# Conflicts:
#	package.json
yusuketk 7 лет назад
Родитель
Сommit
a63385cf73

+ 1 - 0
CHANGES.md

@@ -4,6 +4,7 @@ CHANGES
 ## 3.2.0-RC
 
 * Feature: HackMD integration so that user can simultaneously edit with multiple people
+* Fix: The Initial scroll position is wrong when reloading the page
 
 ## 3.1.14
 

+ 3 - 1
README.md

@@ -41,7 +41,8 @@ Features
   * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
 * **Features**
   * Create hierarchical pages with markdown
-  * Support Authentication with LDAP / Active Directory 
+  * Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
+  * Support Authentication with LDAP / Active Directory
   * Slack Incoming Webhooks Integration
   * [Miscellaneous features](https://github.com/weseek/growi/wiki/Additional-Features)
 * **[Docker Ready][dockerhub]**
@@ -165,6 +166,7 @@ Environment Variables
     * FILE_UPLOAD: `aws` (default), `local`, `none`
 * **Option to integrate with external systems**
     * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
+        * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/management-cookbook/integrate-with-hackmd).**
     * HACKMD_URI_FOR_SERVER: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server from GROWI Express server. If not set, `HACKMD_URI` will be used.
     * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
     * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.

+ 0 - 1
config/webpack.common.js

@@ -22,7 +22,6 @@ module.exports = (options) => {
     entry: Object.assign({
       'js/app':                   './resource/js/app',
       'js/legacy':                './resource/js/legacy/crowi',
-      'js/legacy-form':           './resource/js/legacy/crowi-form',
       'js/legacy-admin':          './resource/js/legacy/crowi-admin',
       'js/legacy-presentation':   './resource/js/legacy/crowi-presentation',
       'js/plugin':                './resource/js/plugin',

+ 1 - 0
lib/crowi/index.js

@@ -276,6 +276,7 @@ Crowi.prototype.setupPassport = function() {
     this.passportService.setupLdapStrategy();
     this.passportService.setupGoogleStrategy();
     this.passportService.setupGitHubStrategy();
+    this.passportService.setupTwitterStrategy(); 
   }
   catch (err) {
     logger.error(err);

+ 13 - 0
lib/form/admin/securityPassportTwitter.js

@@ -0,0 +1,13 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[security:passport-twitter:isEnabled]').trim().toBooleanStrict().required(),
+  field('settingForm[security:passport-twitter:consumerKey]').trim(),
+  field('settingForm[security:passport-twitter:consumerSecret]').trim(),
+  field('settingForm[security:passport-twitter:callbackUrl]').trim(),
+  field('settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
+);

+ 1 - 0
lib/form/index.js

@@ -23,6 +23,7 @@ module.exports = {
     securityPassportLdap: require('./admin/securityPassportLdap'),
     securityPassportGoogle: require('./admin/securityPassportGoogle'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
+    securityPassportTwitter: require('./admin/securityPassportTwitter'),
     markdown: require('./admin/markdown'),
     markdownXss: require('./admin/markdownXss'),
     customcss: require('./admin/customcss'),

+ 12 - 2
lib/locales/en-US/translation.json

@@ -389,13 +389,23 @@
         "name": "Facebook OAuth"
       },
       "Twitter": {
-        "name": "Twitter OAuth"
+        "name": "Twitter OAuth",
+        "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
+        "register_2": "Sign in Twitter",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>%s</code> (where <code>%s</code> is your hostname)",
+        "register_5": "Copy and paste your ClientID and Client Secret above"
       },
       "GitHub": {
         "name": "GitHub OAuth",
         "register_1": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>%s</code> (where <code>%s</code> is your hostname)",
-        "register_3": "Copy and paste your ClientID and Client Secret above"
+        "register_3": "Copy and paste your ClientID and Client Secret above"     
+      },
+      "how_to": {
+        "google": "How to configure Google OAuth?",
+        "github": "How to configure GitHub OAuth?",
+        "twitter": "How to configure Twitter OAuth?"
       }
     }
 	},

+ 11 - 1
lib/locales/ja/translation.json

@@ -407,13 +407,23 @@
         "name": "Facebook OAuth認証"
       },
       "Twitter": {
-        "name": "Twitter OAuth認証"
+        "name": "Twitter OAuth認証",
+        "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
+        "register_2": "Twitterにサインイン",
+        "register_3": "Create New Appをクリック &rightarrow; Application Detailsの各項目を入力",
+        "register_4": "Create your Twitter Applicationで作成",
+        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
       },
       "GitHub": {
         "name": "GitHub OAuth認証",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
         "register_2": "\"Authorization callback URL\"を<code>%s</code>としてGrowiを登録 (<code>%s</code>は環境に合わせて変更してください)",
         "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力"
+      },
+      "how_to": {
+        "google": "Google OAuthの設定方法",
+        "github": "GitHub OAuthの設定方法",
+        "twitter": "Twitter OAuthの設定方法"
       }
     }
   },

+ 6 - 0
lib/models/config.js

@@ -68,6 +68,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
       'security:passport-google:isEnabled' : false,
       'security:passport-github:isEnabled' : false,
+      'security:passport-twitter:isEnabled' : false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
@@ -290,6 +291,11 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   };
 
+   configSchema.statics.isEnabledPassportTwitter = function(config) {
+    const key = 'security:passport-twitter:isEnabled';
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType) {
     const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
     return getValueForCrowiNS(config, key);

+ 28 - 0
lib/routes/admin.js

@@ -1102,6 +1102,34 @@ module.exports = function(crowi, app) {
     return res.json({status: true});
   };
 
+  actions.api.securityPassportTwitterSetting = async(req, res) => {
+    const form = req.form.settingForm;
+
+    if (!req.form.isValid) {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+
+    debug('form content', form);
+    await saveSettingAsync(form);
+    const config = await crowi.getConfig();
+    
+
+    // reset strategy
+    await crowi.passportService.resetTwitterStrategy();
+    // setup strategy
+    if (Config.isEnabledPassportTwitter(config)) {
+      try {
+        await crowi.passportService.setupTwitterStrategy(true);
+      }
+      catch (err) {
+        // reset
+        await crowi.passportService.resetTwitterStrategy();
+        return res.json({status: false, message: err.message});
+      }
+    }
+
+    return res.json({status: true});
+  };
   actions.api.customizeSetting = function(req, res) {
     const form = req.form.settingForm;
 

+ 8 - 1
lib/routes/hackmd.js

@@ -7,6 +7,7 @@ const axios = require('axios');
 const ApiResponse = require('../util/apiResponse');
 
 module.exports = function(crowi, app) {
+  const config = crowi.getConfig();
   const Page = crowi.models.Page;
 
   // load GROWI agent script for HackMD
@@ -33,7 +34,13 @@ module.exports = function(crowi, app) {
       agentScriptContentTpl = swig.compileFile(agentScriptPath);
     }
 
-    const origin = `${req.protocol}://${req.get('host')}`;
+    let origin = `${req.protocol}://${req.get('host')}`;
+
+    // use config.crowi['app:url'] when exist req.headers['x-forwarded-proto'].
+    // refs: lib/crowi/express-init.js
+    if (config.crowi && config.crowi['app:url']) {
+      origin = config.crowi['app:url'];
+    }
 
     // generate definitions to replace
     const definitions = {

+ 3 - 0
lib/routes/index.js

@@ -71,10 +71,13 @@ module.exports = function(crowi, app) {
   // OAuth
   app.post('/_api/admin/security/passport-google' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
   app.post('/_api/admin/security/passport-github' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
+  app.post('/_api/admin/security/passport-twitter' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportTwitter, admin.api.securityPassportTwitterSetting);
   app.get('/passport/google'                      , loginPassport.loginWithGoogle);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub);
+  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
+  app.get('/passport/twitter/callback'             , loginPassport.loginPassportTwitterCallback); 
 
   // markdown admin
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);

+ 37 - 0
lib/routes/login-passport.js

@@ -277,6 +277,41 @@ module.exports = function(crowi, app) {
     });
   };
 
+  const loginWithTwitter = function(req, res, next) {
+    if (!passportService.isTwitterStrategySetup) {
+      debug('TwitterStrategy has not been set up');
+      req.flash('warningMessage', 'TwitterStrategy has not been set up');
+      return next();
+    }
+
+    passport.authenticate('twitter')(req, res);
+  };
+
+  const loginPassportTwitterCallback = async(req, res, next) => {
+    const providerId = 'twitter';
+    const strategyName = 'twitter';
+    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+    const userInfo = {
+      'id': response.id,
+      'username': response.username,
+      'name': response.displayName
+    };
+
+    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
+
+    const user = await externalAccount.getPopulatedUser();
+
+    // login
+    req.logIn(user, err => {
+      if (err) { return next(err) }
+      return loginSuccess(req, res, user);
+    });
+  };
+
+
   const promisifiedPassportAuthentication = (req, res, next, strategyName) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
@@ -336,7 +371,9 @@ module.exports = function(crowi, app) {
     loginWithLocal,
     loginWithGoogle,
     loginWithGitHub,
+    loginWithTwitter,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
+    loginPassportTwitterCallback,
   };
 };

+ 47 - 1
lib/service/passport.js

@@ -4,6 +4,7 @@ const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
 const GoogleStrategy = require('passport-google-auth').Strategy;
 const GitHubStrategy = require('passport-github').Strategy;
+const TwitterStrategy = require('passport-twitter').Strategy;
 
 /**
  * the service class of Passport
@@ -342,7 +343,6 @@ class PassportService {
     this.isGitHubStrategySetup = true;
     debug('GitHubStrategy: setup is done');
   }
-
   /**
    * reset GoogleStrategy
    *
@@ -354,6 +354,52 @@ class PassportService {
     this.isGitHubStrategySetup = false;
   }
 
+  setupTwitterStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isTwitterStrategySetup) {
+      throw new Error('TwitterStrategy has already been set up');
+    }
+
+    const config = this.crowi.config;
+    const Config = this.crowi.model('Config');
+    //this
+    const isTwitterEnabled = Config.isEnabledPassportTwitter(config);
+
+    // when disabled
+    if (!isTwitterEnabled) {
+      return;
+    }
+
+    debug('TwitterStrategy: setting up..');
+    passport.use(new TwitterStrategy({
+      consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
+      consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
+      callbackURL: config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,
+      skipUserProfile: false,
+    }, function(accessToken, refreshToken, profile, done) {
+      if (profile) {
+        return done(null, profile);
+      }
+      else {
+        return done(null, false);
+      }
+    }));
+
+    this.isTwitterStrategySetup = true;
+    debug('TwitterStrategy: setup is done');
+  }
+
+  /**
+   * reset GoogleStrategy
+   *
+   * @memberof PassportService
+   */
+  resetTwitterStrategy() {
+    debug('TwitterStrategy: reset');
+    passport.unuse('twitter');
+    this.isTwitterStrategySetup = false;
+  }
+
   /**
    * setup serializer and deserializer
    *

+ 5 - 0
lib/util/swigFunctions.js

@@ -102,6 +102,11 @@ module.exports = function(crowi, app, req, locals) {
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
   };
 
+  locals.passportTwitterLoginEnabled = function() {
+    var config = crowi.getConfig();
+    return locals.isEnabledPassport() && config.crowi['security:passport-twitter:isEnabled'];
+  };
+
   locals.searchConfigured = function() {
     if (crowi.getSearcher()) {
       return true;

+ 0 - 5
lib/views/_form.html

@@ -1,8 +1,3 @@
-{% block html_head_loading_legacy %}
-  <script src="{{ webpack_asset('js/legacy-form.js') }}" defer></script>
-  {% parent %}
-{% endblock %}
-
 {% if req.form.errors %}
 <div class="alert alert-danger">
   <ul>

+ 3 - 3
lib/views/admin/security.html

@@ -252,10 +252,10 @@
               <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> GitHub</a>
             </li>
             <li class="tbd">
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
+              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
             </li>
             <li class="tbd">
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> (TBD) Twitter</a>
+              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
             </li>
           </ul>
 
@@ -288,7 +288,7 @@
   </div>
 
   <script>
-    $('#generalSetting, #googleSetting, #mechanismSetting, #githubSetting').each(function() {
+    $('#generalSetting, #googleSetting, #mechanismSetting, #githubSetting, #twitterSetting').each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {

+ 1 - 1
lib/views/admin/widget/passport/github.html

@@ -92,7 +92,7 @@
 <hr>
 <h4>
   <i class="fa fa-question-circle" aria-hidden="true"></i>
-  <a href="#collapseHelpForGithubOauth" data-toggle="collapse">How to configure GitHub OAuth?</a>
+  <a href="#collapseHelpForGithubOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.github") }}</a>
 </h4>
 <ol id="collapseHelpForGithubOauth" class="collapse">
   <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>

+ 1 - 1
lib/views/admin/widget/passport/google-oauth.html

@@ -92,7 +92,7 @@
 <hr>
 <h4>
   <i class="fa fa-question-circle" aria-hidden="true"></i>
-  <a href="#collapseHelpForGoogleOauth" data-toggle="collapse">How to configure Google OAuth?</a>
+  <a href="#collapseHelpForGoogleOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.google") }}</a>
 </h4>
 <ol id="collapseHelpForGoogleOauth" class="collapse">
   <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>

+ 115 - 4
lib/views/admin/widget/passport/twitter.html

@@ -1,6 +1,117 @@
-<form action="" method="post" class="form-horizontal passportStrategy" id="twitterOauthSetting" role="form">
-  <fieldset>
-    <legend>Twitter OAuth {{ t("security_setting.configuration") }}</legend>
-    <p class="well">(TBD)</p>
+<form action="/_api/admin/security/passport-twitter" method="post" class="form-horizontal passportStrategy" id="twitterSetting" role="form"
+    {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+  <legend class="alert-anchor">{{ t("security_setting.OAuth.Twitter.name") }}{{ t("security_setting.configuration") }}</legend>
+
+  {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
+  {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
+  
+  <div class="form-group">
+    <label for="{{nameForIsTwitterEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Twitter.name") }}</label>
+    <div class="col-xs-6">
+      <div class="btn-group btn-toggle" data-toggle="buttons">
+        <label class="btn btn-default btn-rounded btn-outline {% if isTwitterEnabled %}active{% endif %}" data-active-class="primary">
+          <input name="{{nameForIsTwitterEnabled}}" value="true" type="radio"
+              {% if true === isTwitterEnabled %}checked{% endif %}> ON
+        </label>
+        <label class="btn btn-default btn-rounded btn-outline {% if !isTwitterEnabled %}active{% endif %}" data-active-class="default">
+          <input name="{{nameForIsTwitterEnabled}}" value="false" type="radio"
+              {% if !isTwitterEnabled %}checked{% endif %}> OFF
+        </label>
+      </div>
+    </div>
+  </div>
+  <fieldset id="passport-twitter-hide-when-disabled" {%if !isTwitterEnabled %}style="display: none;"{% endif %}>
+
+
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:consumerKey]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerKey]" value="{{ settingForm['security:passport-twitter:consumerKey'] || '' }}">
+        <p class="help-block">
+          <small>
+                {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_KEY") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:consumerSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:consumerSecret]" value="{{ settingForm['security:passport-twitter:consumerSecret'] || '' }}">
+        <p class="help-block">
+          <small>
+             {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CONSUMER_SECRET") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-twitter:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text" name="settingForm[security:passport-twitter:callbackUrl]" value="{{ settingForm['security:passport-twitter:callbackUrl'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CALLBACK_URL") }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    
+    <div class="form-group">
+      <div class="col-xs-6 col-xs-offset-3">
+        <div class="checkbox checkbox-info">
+          <input type="checkbox" id="bindByUserName-Twitter" name="settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]" value="1"
+              {% if settingForm['security:passport-twitter:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
+          <label for="bindByUserName-Twitter">
+            {{ t("security_setting.Treat username matching as identical", "username") }}
+          </label>
+          <p class="help-block">
+            <small>
+              {{ t("security_setting.Treat username matching as identical_warn", "username") }}
+            </small>
+          </p>
+        </div>
+      </div>
+    </div>
+
   </fieldset>
+
+  <div class="form-group" id="btn-update">
+    <div class="col-xs-offset-3 col-xs-6">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
+      <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+    </div>
+  </div>
+
 </form>
+
+{# Help Section #}
+<hr>
+<h4>
+  <i class="fa fa-question-circle" aria-hidden="true"></i>
+  <a href="#collapseHelpForTwitterOauth" data-toggle="collapse">{{ t("security_setting.OAuth.how_to.twitter") }}</a>
+</h4>
+<ol id="collapseHelpForTwitterOauth" class="collapse">
+  <li>{{ t("security_setting.OAuth.Twitter.register_1", "https://apps.twitter.com/", "Twitter Application Management") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_2") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_3") }}</li>
+  <li>{{ t("security_setting.OAuth.Twitter.register_4", "https://${growi.host}/passport/twitter/callback", "${growi.host}") }}</li>
+</ol>
+
+<script>
+  $('input[name="settingForm[security:passport-twitter:isEnabled]"]').change(function() {
+      const isEnabled = ($(this).val() === "true");
+
+      if (isEnabled) {
+        $('#passport-twitter-hide-when-disabled').show(400);
+      }
+      else {
+        $('#passport-twitter-hide-when-disabled').hide(400);
+      }
+    });
+</script>
+

+ 2 - 2
lib/views/widget/user_page_header.html

@@ -3,9 +3,9 @@
 
     <h4 id="revision-path"></h4>
 
-    <div class="users-meta d-flex align-items-center">
+    <div class="users-info d-flex align-items-center">
       <img src="{{ pageUser|picture }}" class="picture img-circle">
-      <div class="m-l-30" style="flex: 1;">
+      <div class="users-meta" style="flex: 1;">
         <div class="d-flex align-items-center">
           <h1>
             {{ pageUser.name }}

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.2.0-RC3",
+  "version": "3.2.0-RC4",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -104,6 +104,7 @@
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "request": "^2.87.0",
+    "passport-twitter": "^1.0.4",
     "rimraf": "^2.6.1",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",

+ 10 - 8
resource/js/components/PageComment/CommentForm.js

@@ -54,8 +54,8 @@ export default class CommentForm extends React.Component {
     this.handleSelect = this.handleSelect.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
     this.onUpload = this.onUpload.bind(this);
-    this.onChannelChange = this.onChannelChange.bind(this);
-    this.onSlackOnChange = this.onSlackOnChange.bind(this);
+    this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
+    this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
   }
 
   updateState(value) {
@@ -74,11 +74,11 @@ export default class CommentForm extends React.Component {
     this.renderHtml(this.state.comment);
   }
 
-  onSlackOnChange(value) {
+  onSlackEnabledFlagChange(value) {
     this.setState({isSlackEnabled: value});
   }
 
-  onChannelChange(value) {
+  onSlackChannelsChange(value) {
     this.setState({slackChannels: value});
   }
 
@@ -269,11 +269,13 @@ export default class CommentForm extends React.Component {
                 </div>
                 <div className="comment-submit">
                   <div className="d-flex">
+                    <label style={{flex: 1}}>
                     { this.state.key == 1 &&
-                      <label style={{flex: 1}}>
+                      <span>
                         <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateStateCheckbox} /> Markdown
-                      </label>
+                      </span>
                     }
+                    </label>
                     <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
                     { this.state.hasSlackConfig &&
                       <div className="form-inline align-self-center mr-md-2">
@@ -281,10 +283,10 @@ export default class CommentForm extends React.Component {
                           crowi={this.props.crowi}
                           pageId={this.props.pageId}
                           pagePath={this.props.pagePath}
-                          onSlackOnChange={this.onSlackOnChange}
-                          onChannelChange={this.onChannelChange}
                           isSlackEnabled={this.state.isSlackEnabled}
                           slackChannels={this.state.slackChannels}
+                          onEnabledFlagChange={this.onSlackEnabledFlagChange}
+                          onChannelChange={this.onSlackChannelsChange}
                         />
                       </div>
                     }

+ 1 - 4
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -114,9 +114,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
   componentDidMount() {
     // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
     this.getCodeMirror().codeMirrorEditor = this;
-
-    // initialize caret line
-    this.setCaretLine(0);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -516,7 +513,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
                 - リスト 1<br />
                 &nbsp;&nbsp;&nbsp;&nbsp;- リスト 1_1<br />
                 - リスト 2<br />
-                1. 番号付きリスト 1
+                1. 番号付きリスト 1<br />
                 1. 番号付きリスト 2
               </p>
               <hr />

+ 0 - 5
resource/js/components/PageEditor/Editor.js

@@ -32,11 +32,6 @@ export default class Editor extends AbstractEditor {
     this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
   }
 
-  componentDidMount() {
-    // initialize caret line
-    this.setCaretLine(0);
-  }
-
   getEditorSubstance() {
     return this.props.isMobile
       ? this.refs.taEditor

+ 0 - 14
resource/js/legacy/crowi-form.js

@@ -1,14 +0,0 @@
-// const pagePath= $('#content-main').data('path');
-
-// /**
-//  * DOM ready
-//  */
-// $(function() {
-
-//   $('#page-form').on('submit', function(e) {
-//     // avoid message
-//     // isFormChanged = false;
-//     window.crowi.clearDraft(pagePath);
-//   });
-
-// });

+ 23 - 20
resource/js/legacy/crowi.js

@@ -159,6 +159,28 @@ Crowi.handleKeyCtrlSlashHandler = (event) => {
   event.preventDefault();
 };
 
+Crowi.initAffix = () => {
+  const $affixContent = $('#page-header');
+  if ($affixContent.length > 0) {
+    const $affixContentContainer = $('.row.bg-title');
+    const containerHeight = $affixContentContainer.outerHeight(true);
+    $affixContent.affix({
+      offset: {
+        top: function() {
+          return $('.navbar').outerHeight(true) + containerHeight;
+        }
+      }
+    });
+    $('[data-affix-disable]').on('click', function(e) {
+      const $elm = $($(this).data('affix-disable'));
+      $(window).off('.affix');
+      $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
+      return false;
+    });
+    $affixContentContainer.css({'min-height': containerHeight});
+  }
+};
+
 Crowi.initSlimScrollForRevisionToc = () => {
   const revisionTocElem = document.querySelector('.growi .revision-toc');
   const tocContentElem = document.querySelector('.growi .revision-toc .markdownIt-TOC');
@@ -559,26 +581,6 @@ $(function() {
       top.location.href = `${path}#edit`;
     });
 
-    // header affix
-    const $affixContent = $('#page-header');
-    if ($affixContent.length > 0) {
-      const $affixContentContainer = $('.row.bg-title');
-      const containerHeight = $affixContentContainer.outerHeight(true);
-      $affixContent.affix({
-        offset: {
-          top: function() {
-            return $('.navbar').outerHeight(true) + containerHeight;
-          }
-        }
-      });
-      $('[data-affix-disable]').on('click', function(e) {
-        const $elm = $($(this).data('affix-disable'));
-        $(window).off('.affix');
-        $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
-        return false;
-      });
-    }
-
     // Like
     const $likeButton = $('.like-button');
     const $likeCount = $('#like-count');
@@ -807,6 +809,7 @@ window.addEventListener('load', function(e) {
   Crowi.modifyScrollTop();
   Crowi.setCaretLineAndFocusToEditor();
   Crowi.initSlimScrollForRevisionToc();
+  Crowi.initAffix();
 });
 
 window.addEventListener('hashchange', function(e) {

+ 1 - 1
resource/styles/scss/_on-edit.scss

@@ -23,7 +23,7 @@ body.on-edit {
   .row.page-comments-row,
   .row.page-attachments-row,
   .row.not-found-message-row,
-  .users-meta,
+  .users-info,
   .user-page-content-container,
   .portal-form-button,
   .alert-info.alert-moved,

+ 26 - 0
resource/styles/scss/_user.scss

@@ -1,7 +1,16 @@
 .main-container {
   .user-page-header {
 
+    #revision-path {
+      margin-bottom: 0;
+    }
+
+    .users-meta {
+      margin-left: 30px;
+    }
+
     h1 {
+      margin: 0;
       font-size: 2.5em;
       color: #666;
     }
@@ -35,7 +44,24 @@
         padding: 8px;
       }
     }
+  }
+
+  // affix
+  .user-page-header.affix {
 
+    .users-meta {
+      margin-left: 15px;
+    }
+
+    h1 {
+      font-size: 1.5em;
+      line-height: 30px;
+    }
+
+    .picture {
+      height: 48px;
+      width: 48px;
+    }
   }
 }
 

+ 25 - 0
yarn.lock

@@ -6258,6 +6258,14 @@ passport-local@^1.0.0:
   dependencies:
     passport-strategy "1.x.x"
 
+passport-oauth1@1.x.x:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918"
+  dependencies:
+    oauth "0.9.x"
+    passport-strategy "1.x.x"
+    utils-merge "1.x.x"
+
 passport-oauth2@1.x.x:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
@@ -6271,6 +6279,13 @@ passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
 
+passport-twitter@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/passport-twitter/-/passport-twitter-1.0.4.tgz#01a799e1f760bf2de49f2ba5fba32282f18932d7"
+  dependencies:
+    passport-oauth1 "1.x.x"
+    xtraverse "0.1.x"
+
 passport@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811"
@@ -9038,6 +9053,10 @@ xmlbuilder@4.2.1, xmlbuilder@^4.1.0:
   dependencies:
     lodash "^4.0.0"
 
+xmldom@0.1.x:
+  version "0.1.27"
+  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+
 xmlhttprequest-ssl@~1.5.4:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz#04f560915724b389088715cc0ed7813e9677bf57"
@@ -9066,6 +9085,12 @@ xtend@~2.1.1:
   dependencies:
     object-keys "~0.4.0"
 
+xtraverse@0.1.x:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/xtraverse/-/xtraverse-0.1.0.tgz#b741bad018ef78d8a9d2e83ade007b3f7959c732"
+  dependencies:
+    xmldom "0.1.x"
+
 y18n@^3.2.0, y18n@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"