Merge branch 'master' into DepauMD

This commit is contained in:
Davide Depau 2019-06-05 11:46:10 +02:00
commit d59212ea8b
Signed by: depau
GPG key ID: C7D999B6A55EFE86
82 changed files with 2240 additions and 2053 deletions

View file

@ -2,7 +2,7 @@
"presets": [
["env", {
"targets": {
"node": "6",
"node": "8",
"uglify": true
}
}]

View file

@ -10,6 +10,7 @@ module.exports = {
// wrong.
"import/first": ["warn"],
"indent": ["warn"],
"no-console": ["warn"],
"no-multiple-empty-lines": ["warn"],
"no-multi-spaces": ["warn"],
"object-curly-spacing": ["warn"],

View file

@ -1,40 +1,40 @@
language: node_js
dist: trusty
dist: xenial
cache: yarn
env:
global:
- CXX=g++-4.8
- YARN_VERSION=1.15.2
jobs:
include:
- env: task=npm-test
node_js:
- 6
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
- export PATH="$HOME/.yarn/bin:$PATH"
- env: task=npm-test
node_js:
- 8
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
- export PATH="$HOME/.yarn/bin:$PATH"
- env: task=npm-test
- stage: Static Tests
name: eslint
node_js:
- 10
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
- export PATH="$HOME/.yarn/bin:$PATH"
- env: task=ShellCheck
script:
- yarn run eslint
- name: ShellCheck
script:
- shellcheck bin/heroku bin/setup
language: generic
- env: task=json-lint
- name: json-lint
addons:
apt:
packages:
- jq
script:
- npm run jsonlint
- yarn run jsonlint
language: generic
- stage: Dynamic Tests
name: Node.js 8
node_js:
- 8
script:
- yarn run mocha-suite
- name: Node.js 10
node_js:
- 10
script:
- yarn run mocha-suite
- name: Node.js 12
node_js:
- 12
script:
- yarn run mocha-suite

View file

@ -5,6 +5,7 @@ CodiMD
[![build status][travis-image]][travis-url]
[![version][github-version-badge]][github-release-page]
[![POEditor][poeditor-image]][poeditor-url]
[![Mastodon][social-mastodon-image]][social-mastodon]
CodiMD lets you create real-time collaborative markdown notes. You can test-drive
it by visiting our [CodiMD demo server][codimd-demo].
@ -98,3 +99,5 @@ Licensed under AGPLv3. For our list of contributors, see [AUTHORS](AUTHORS).
[codimd-demo-features]: https://demo.codimd.org/features
[codimd-community]: https://community.codimd.org
[codimd-community-calls]: https://community.codimd.org/t/codimd-community-call/19
[social-mastodon]: https://social.codimd.org/mastodon
[social-mastodon-image]: https://img.shields.io/badge/social-mastodon-3c99dc.svg

2
app.js
View file

@ -113,7 +113,7 @@ if (config.csp.enable) {
}
i18n.configure({
locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr'],
locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr', 'vi'],
cookie: 'locale',
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
directory: path.join(__dirname, '/locales'),

View file

@ -32,6 +32,7 @@ to `config.json` before filling in your own details.
| `imageUploadType` | `imgur`, `s3`, `minio`, `azure`, `lutim` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md)|
| `sourceURL` | `https://github.com/codimd/server/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
| `staticCacheTime` | `1 * 24 * 60 * 60 * 1000` | static file cache time |
| `tooBusyLag` | `70` | CPU time for one eventloop tick until node throttles connections. (milliseconds) |
| `heartbeatInterval` | `5000` | socket.io heartbeat interval |
| `heartbeatTimeout` | `10000` | socket.io heartbeat timeout |
| `documentMaxLength` | `100000` | note max length |

View file

@ -35,6 +35,7 @@ defaultNotePath can't be set from env-vars
| `CMD_FORBIDDEN_NOTE_IDS` | `'robots.txt'` | disallow creation of notes, even if `CMD_ALLOW_FREEURL` is `true` |
| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio`, `lutim` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. |
| `CMD_SOURCE_URL` | `https://github.com/codimd/server/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
| `CMD_TOOBUSY_LAG` | `70` | CPU time for one eventloop tick until node throttles connections. (milliseconds) |
## CodiMD Location

View file

@ -0,0 +1,50 @@
Keycloak/Red Hat SSO (self-hosted)
===
## Prerequisites
This guide assumes you have run and configured Keycloak. If you'd like to meet this prerequisite quickly, it can be achieved by running a `jboss/keycloak` container and attaching it to your network. Set the environment variables KEYCLOAK_USER and `KEYCLOAK_PASSWORD`, and expose port 8080.
Where HTTPS is specified throughout, use HTTP instead. You may also have to specify the exposed port, 8080.
## Steps
1. Sign in to the administration portal for your Keycloak instance at https://keycloak.example.com/auth/admin/master/console
You may note that a separate realm is specified throughout this tutorial. It is best practice not to use the master realm, as it normally contains the realm-management client that federates access using the policies and permissions you can create.
2. Navigate to the client management page at `https://keycloak.example.com/auth/admin/master/console/#/realms/your-realm/clients` (admin permissions required)
3. Click **Create** to create a new client and fill out the registration form. You should set the Root URL to the fully qualified public URL of your CodiMD instance.
4. Click **Save**
5. Set the **Access Type** of the client to `confidential`. This will make your client require a client secret upon authentication.
---
### Additional steps to circumvent generic OAuth2 issue:
1. Select Client Scopes from the sidebar, and begin to create a new client scope using the Create button.
2. Ensure that the **Name** field is set to `id`.
3. Create a new mapper under the Mappers tab. This should reference the User Property `id`. `Claim JSON Type` should be String and all switches below should be enabled. Save the mapper.
4. Go to the client you set up in the previous steps using the Clients page, then choose the Client Scopes tab. Apply the scope you've created. This should mitigate errors as seen in [codimd/server#56](https://github.com/codimd/server/issues/56), as the `/userinfo` endpoint should now bring back the user's ID under the `id` key as well as `sub`.
---
6. In the `docker-compose.yml` add the following environment variables to `app:` `environment:`
```
CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/userinfo
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/token
CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/auth
CMD_OAUTH2_CLIENT_ID=<your client ID>
CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
CMD_OAUTH2_PROVIDERNAME=Keycloak
CMD_DOMAIN=<codimd.example.com>
CMD_PROTOCOL_USESSL=true
CMD_URL_ADDPORT=false
```
7. Run `docker-compose up -d` to apply your settings.
8. Sign in to your CodiMD using your Keycloak ID

View file

@ -1,6 +1,10 @@
Migrations and Notable Changes
===
## Migrating to 1.4.0
We dropped support for node 6 with this version. If you have any trouble running this version, please double check that you are running at least node 8!
## Migrating to 1.3.2
This is not a breaking change, but to stay up to date with the community

View file

@ -16,7 +16,7 @@ CodiMD by docker container
The easiest way to setup CodiMD using docker are using the following three commands:
```sh
git clone https://github.com/codimd/container.git
git clone https://github.com/codimd/container.git codimd-container
cd codimd-container
docker-compose up
```

View file

@ -3,11 +3,10 @@ Manual Installation
## Requirements on your server
- Node.js 6.x or up (test up to 7.5.0) and <10.x
- Node.js 8.5 or up
- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8`
- npm (and its dependencies, [node-gyp](https://github.com/nodejs/node-gyp#installation))
- yarn
- `libssl-dev` for building scrypt (see [here](https://github.com/ml1nk/node-scrypt/blob/master/README.md#installation-instructions) for further information)
- Bash (for the setup script)
- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM

View file

@ -56,6 +56,8 @@ module.exports = {
// socket.io
heartbeatInterval: 5000,
heartbeatTimeout: 10000,
// too busy timeout
tooBusyLag: 70,
// document
documentMaxLength: 100000,
// image upload setting, available options are imgur/s3/filesystem/azure/lutim

View file

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
module.exports = {
sourceURL: process.env.CMD_SOURCE_URL,
@ -33,6 +33,7 @@ module.exports = {
dbURL: process.env.CMD_DB_URL,
sessionSecret: process.env.CMD_SESSION_SECRET,
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
tooBusyLag: toIntegerConfig(process.env.CMD_TOOBUSY_LAG),
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
imgur: {
clientID: process.env.CMD_IMGUR_CLIENTID

View file

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
module.exports = {
domain: process.env.HMD_DOMAIN,

View file

@ -4,11 +4,11 @@
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const {merge} = require('lodash')
const { merge } = require('lodash')
const deepFreeze = require('deep-freeze')
const {Environment, Permission} = require('./enum')
const { Environment, Permission } = require('./enum')
const logger = require('../logger')
const {getGitCommit, getGitHubURL} = require('./utils')
const { getGitCommit, getGitHubURL } = require('./utils')
const appRootPath = path.resolve(__dirname, '../../')
const env = process.env.NODE_ENV || Environment.development
@ -17,7 +17,7 @@ const debugConfig = {
}
// Get version string from package.json
const {version, repository} = require(path.join(appRootPath, 'package.json'))
const { version, repository } = require(path.join(appRootPath, 'package.json'))
const commitID = getGitCommit(appRootPath)
const sourceURL = getGitHubURL(repository.url, commitID || version)

View file

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig} = require('./utils')
const { toBooleanConfig } = require('./utils')
module.exports = {
debug: toBooleanConfig(process.env.DEBUG),

View file

@ -30,14 +30,14 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
if (typeof email !== 'string') {
email = '' + name + '@example.com'
}
name=encodeURIComponent(name)
name = encodeURIComponent(name)
let hash = crypto.createHash('md5')
hash.update(email.toLowerCase())
let hexDigest = hash.digest('hex')
if (email !== '' && config.allowGravatar) {
photo = 'https://cdn.libravatar.org/avatar/' + hexDigest;
photo = 'https://cdn.libravatar.org/avatar/' + hexDigest
if (big) {
photo += '?s=400'
} else {

View file

@ -1,5 +1,5 @@
'use strict'
const {createLogger, format, transports} = require('winston')
const { createLogger, format, transports } = require('winston')
const logger = createLogger({
level: 'debug',

View file

@ -22,6 +22,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -9,6 +9,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -9,6 +9,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -5,6 +5,7 @@ module.exports = {
return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING)
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -17,6 +17,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -18,6 +18,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -3,6 +3,7 @@ module.exports = {
up: function (queryInterface, Sequelize) {
return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -4,6 +4,7 @@ module.exports = {
return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () {
return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) {
if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error
@ -11,6 +12,7 @@ module.exports = {
})
}).catch(function (error) {
if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') {
// eslint-disable-next-line no-console
console.log('Migration has already run… ignoring.')
} else {
throw error

View file

@ -1,16 +1,16 @@
'use strict'
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT})
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
}
}

View file

@ -2,12 +2,12 @@
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT})
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
}
}

View file

@ -2,10 +2,10 @@
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private')})
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'locked', 'private')})
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
}
}

View file

@ -3,7 +3,7 @@
var fs = require('fs')
var path = require('path')
var Sequelize = require('sequelize')
const {cloneDeep} = require('lodash')
const { cloneDeep } = require('lodash')
// core
var config = require('../config')

View file

@ -1,11 +1,20 @@
'use strict'
// external modules
var Sequelize = require('sequelize')
var scrypt = require('scrypt')
const Sequelize = require('sequelize')
const crypto = require('crypto')
if (!crypto.scrypt) {
// polyfill for node.js 8.0, see https://github.com/chrisveness/scrypt-kdf#openssl-implementation
const scryptAsync = require('scrypt-async')
crypto.scrypt = function (password, salt, keylen, options, callback) {
const opt = Object.assign({}, options, { dkLen: keylen })
scryptAsync(password, salt, opt, (derivedKey) => callback(null, Buffer.from(derivedKey)))
}
}
const scrypt = require('scrypt-kdf')
// core
var logger = require('../logger')
var {generateAvatarURL} = require('../letter-avatars')
const logger = require('../logger')
const { generateAvatarURL } = require('../letter-avatars')
module.exports = function (sequelize, DataTypes) {
var User = sequelize.define('User', {
@ -41,20 +50,12 @@ module.exports = function (sequelize, DataTypes) {
}
},
password: {
type: Sequelize.TEXT,
set: function (value) {
var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex')
this.setDataValue('password', hash)
}
type: Sequelize.TEXT
}
}, {
instanceMethods: {
verifyPassword: function (attempt) {
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
return this
} else {
return false
}
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
}
},
classMethods: {
@ -140,6 +141,9 @@ module.exports = function (sequelize, DataTypes) {
case 'saml':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
break
default:
photo = generateAvatarURL(profile.username)
break
}
return photo
},
@ -153,5 +157,19 @@ module.exports = function (sequelize, DataTypes) {
}
})
function updatePasswordHashHook (user, options, done) {
// suggested way to hash passwords to be able to do this asynchronously:
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
if (!user.changed('password')) { return done() }
scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
user.setDataValue('password', keyBuf.toString('hex'))
done()
})
}
User.beforeCreate(updatePasswordHashHook)
User.beforeUpdate(updatePasswordHashHook)
return User
}

View file

@ -18,7 +18,7 @@ var utils = require('./utils')
// public
var response = {
errorForbidden: function (res) {
const {req} = res
const { req } = res
if (req.user) {
responseError(res, '403', 'Forbidden', 'oh no.')
} else {

View file

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let dropboxAuth = module.exports = Router()

View file

@ -7,8 +7,8 @@ const LocalStrategy = require('passport-local').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {setReturnToFromReferer} = require('../utils')
const {urlencodedParser} = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
let emailAuth = module.exports = Router()
@ -23,8 +23,14 @@ passport.use(new LocalStrategy({
}
}).then(function (user) {
if (!user) return done(null, false)
if (!user.verifyPassword(password)) return done(null, false)
user.verifyPassword(password).then(verified => {
if (verified) {
return done(null, user)
} else {
logger.warn('invalid password given for %s', user.email)
return done(null, false)
}
})
}).catch(function (err) {
logger.error(err)
return done(err)

View file

@ -5,7 +5,7 @@ const passport = require('passport')
const FacebookStrategy = require('passport-facebook').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let facebookAuth = module.exports = Router()

View file

@ -5,7 +5,7 @@ const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const config = require('../../../config')
const response = require('../../../response')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let githubAuth = module.exports = Router()

View file

@ -5,7 +5,7 @@ const passport = require('passport')
const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../../config')
const response = require('../../../response')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let gitlabAuth = module.exports = Router()

View file

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
var GoogleStrategy = require('passport-google-oauth20').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let googleAuth = module.exports = Router()
@ -12,14 +12,14 @@ passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.serverURL + '/auth/google/callback',
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
}, passportGeneralCallback))
googleAuth.get('/auth/google', function (req, res, next) {
setReturnToFromReferer(req)
passport.authenticate('google', { scope: ['profile'] })(req, res, next)
})
// google auth callback
// google auth callback
googleAuth.get('/auth/google/callback',
passport.authenticate('google', {
successReturnToOrRedirect: config.serverURL + '/',

View file

@ -6,8 +6,8 @@ const LDAPStrategy = require('passport-ldapauth')
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {setReturnToFromReferer} = require('../utils')
const {urlencodedParser} = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
let ldapAuth = module.exports = Router()

View file

@ -5,7 +5,7 @@ const passport = require('passport')
const Mattermost = require('mattermost')
const OAuthStrategy = require('passport-oauth2').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const mattermost = new Mattermost.Client()

View file

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let oauth2Auth = module.exports = Router()

View file

@ -6,8 +6,8 @@ const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {urlencodedParser} = require('../../utils')
const {setReturnToFromReferer} = require('../utils')
const { urlencodedParser } = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
let openIDAuth = module.exports = Router()

View file

@ -6,7 +6,7 @@ const SamlStrategy = require('passport-saml').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {urlencodedParser} = require('../../utils')
const { urlencodedParser } = require('../../utils')
const fs = require('fs')
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }

View file

@ -5,7 +5,7 @@ const passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let twitterAuth = module.exports = Router()

View file

@ -2,7 +2,7 @@
const Router = require('express').Router
const {urlencodedParser} = require('./utils')
const { urlencodedParser } = require('./utils')
const history = require('../history')
const historyRouter = module.exports = Router()

View file

@ -3,7 +3,7 @@ const fs = require('fs')
const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const Minio = require('minio')

View file

@ -3,7 +3,7 @@ const fs = require('fs')
const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const AWS = require('aws-sdk')

View file

@ -3,6 +3,9 @@
const toobusy = require('toobusy-js')
const response = require('../../response')
const config = require('../../config')
toobusy.maxLag(config.tooBusyLag)
module.exports = function (req, res, next) {
if (toobusy()) {

View file

@ -4,7 +4,7 @@ const Router = require('express').Router
const response = require('../response')
const {markdownParser} = require('./utils')
const { markdownParser } = require('./utils')
const noteRouter = module.exports = Router()

View file

@ -8,7 +8,7 @@ const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const {urlencodedParser} = require('./utils')
const { urlencodedParser } = require('./utils')
const statusRouter = module.exports = Router()

View file

@ -8,7 +8,7 @@ const response = require('../response')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const {generateAvatar} = require('../letter-avatars')
const { generateAvatar } = require('../letter-avatars')
const UserRouter = module.exports = Router()

View file

@ -1,6 +1,6 @@
{
"Collaborative markdown notes": "Notas colaborativas en Markdown",
"Realtime collaborative markdown notes on all platforms.": "Notas colaborativas en Markdown para todas las plataformas.",
"Realtime collaborative markdown notes on all platforms.": "Notas colaborativas en Markdown para todas las plataformas en tiempo real.",
"Best way to write and share your knowledge in markdown.": "La mejor forma de escribir y compartir tu conocimiento en Markdown.",
"Intro": "Introducción",
"History": "Historia",
@ -29,6 +29,8 @@
"Import from browser": "Importar del navegador",
"Releases": "Versiones",
"Are you sure?": "¿Estás seguro?",
"Do you really want to delete this note?": "¿Realmente quieres eliminar esta nota?",
"All users will lose their connection.": "Todos los usuarios perderán su conexión.",
"Cancel": "Cancelar",
"Yes, do it!": "Si, ¡hazlo!",
"Choose method": "Elegir método",
@ -60,6 +62,7 @@
"Refresh": "Recargar",
"Contacts": "Contactos",
"Report an issue": "Reportar un problema",
"Meet us on %s": "Encuéntranos en %s",
"Send us email": "Enviarnos un email",
"Documents": "Documentos",
"Features": "Funciones",
@ -100,5 +103,19 @@
"Select From Available Snippets": "Elegir de un Snippet disponible",
"OR": "O",
"Export to Snippet": "Exportar a Snippet",
"Select Visibility Level": "Elegir el nivel de visibilidad"
"Select Visibility Level": "Elegir el nivel de visibilidad",
"Night Theme": "Modo nocturno",
"Follow us on %s and %s.": "Síguenos en %s, y %s.",
"Privacy": "Privacidad",
"Terms of Use": "Términos de uso",
"Do you really want to delete your user account?": "¿Estás seguro que quieres eliminar tu cuenta de usuario?",
"This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Esta acción eliminará tu cuenta, todas tus notas y las referencias a tu cuenta desde otras notas.",
"Delete user": "Eliminar usuario",
"Export user data": "Exportar información de usuario",
"Help us translating on %s": "Ayúdanos traduciendo en %s",
"Source Code": "Código fuente",
"Register": "Registrar",
"Powered by %s": "Desarrollado por %s.",
"Help us translating": "Ayúdanos traduciendo",
"Join the community": "Únete a la comunidad"
}

121
locales/vi.json Normal file
View file

@ -0,0 +1,121 @@
{
"Collaborative markdown notes": "Cộng tác ghi chú markdown",
"Realtime collaborative markdown notes on all platforms.": "Cộng tác ghi chú markdown đa nền tảng thời gian thực",
"Best way to write and share your knowledge in markdown.": "Nền tảng tốt nhất để viết và chia sẻ markdown",
"Intro": "Giới thiệu",
"History": "Lịch sử",
"New guest note": "Khách mới",
"Collaborate with URL": "Cộng tác thời gian thực",
"Support charts and MathJax": "Làm việc với biểu đồ và MathJax",
"Support slide mode": "Hỗ trợ chế độ slide",
"Sign In": "Đăng nhập",
"Below is the history from browser": "Dưới đây là lịch sử của trình duyệt",
"Welcome!": "Chào mừng bạn!",
"New note": "Tạo mới ghi chú",
"or": "hoặc",
"Sign Out": "Đăng xuất",
"Explore all features": "Khám phá tất cả tính năng",
"Select tags...": "Chọn tag",
"Search keyword...": "Tìm kiếm",
"Sort by title": "Sắp xếp theo tiêu đề",
"Title": "Tiêu đề",
"Sort by time": "Sắp xếp theo thời gian",
"Time": "Thời gian",
"Export history": "Xuất lịch sử",
"Import history": "Nhập lịch sử",
"Clear history": "Xóa lịch sử",
"Refresh history": "Làm mới lịch sử",
"No history": "Không có lịch sử",
"Import from browser": "Nhập từ trình duyệt",
"Releases": "Xuất bản",
"Are you sure?": "Bạn có chắc chắn không ?",
"Do you really want to delete this note?": "Bạn có thực sự muốn xóa ghi chú này ?",
"All users will lose their connection.": "Tất cả người dùng sẽ mất liên kết này.",
"Cancel": "Hủy",
"Yes, do it!": "Đồng ý",
"Choose method": "Chọn phương thức",
"Sign in via %s": "Đăng nhấp với %s",
"New": "Mới",
"Publish": "Xuất bản",
"Extra": "Extra",
"Revision": "Sửa đổi",
"Slide Mode": "Chế độ slide",
"Export": "Xuất",
"Import": "Nhập",
"Clipboard": "Clipboard",
"Download": "Tải xuống",
"Raw HTML": "Raw HTML",
"Edit": "Sửa",
"View": "Hiện",
"Both": "Cả hai",
"Help": "Trợ giúp",
"Upload Image": "Tải ảnh lên",
"Menu": "Menu",
"This page need refresh": "Trang này cần được làm mới",
"You have an incompatible client version.": "Phiên bản của client không tương thích.",
"Refresh to update.": "Làm mới để cập nhập.",
"New version available!": "Phiên bản mới đã có sẵn.",
"See releases notes here": "Xem ghi chú xuất bản ở đây.",
"Refresh to enjoy new features.": "Làm mới để trải nghiệm tính năng mới.",
"Your user state has changed.": "Trạng thái người dùng bị thay đổi.",
"Refresh to load new user state.": "Làm mới để cập nhập trạng thái người dùng mới.",
"Refresh": "Làm mới",
"Contacts": "Liên Lạc",
"Report an issue": "Báo cáo vấn đề",
"Meet us on %s": "Gặp chúng tôi ở %s",
"Send us email": "Gửi email cho chúng tôi",
"Documents": "Tài liệu",
"Features": "Tính năng",
"YAML Metadata": "YAML Metadata",
"Slide Example": "Slide ví dụ",
"Cheatsheet": "Cheetsheet",
"Example": "Ví dụ",
"Syntax": "Cú pháp",
"Header": "Đầu đề",
"Unordered List": "Danh sách chưa sắp xếp",
"Ordered List": "Danh sách đã sắp xếp",
"Todo List": "Checklist",
"Blockquote": "Blockquote",
"Bold font": "Bôi đậm",
"Italics font": "In nghiêng",
"Strikethrough": "Gạch ngang",
"Inserted text": "Gạch chân",
"Marked text": "Hightlight",
"Link": "Liên kết",
"Image": "Ảnh",
"Code": "Code",
"Externals": "Externals",
"This is a alert area.": "Đây là khu vực cảnh báo",
"Revert": "Trở lại như cũ",
"Import from clipboard": "Thêm từ clipboard",
"Paste your markdown or webpage here...": "Dán markdown hoặc webpage ở đây ...",
"Clear": "Xóa",
"This note is locked": "Ghi chú này bị khóa.",
"Sorry, only owner can edit this note.": "Xin lỗi, chỉ chủ sở hữu có thể xóa note.",
"OK": "Đồng ý",
"Reach the limit": "Đạt giới hạn",
"Sorry, you've reached the max length this note can be.": "Rất tiếc, bạn đã đạt tới độ dài tối đa ",
"Please reduce the content or divide it to more notes, thank you!": "Vui lòng rút ngắn ghi chú",
"Import from Gist": "Nhập từ Gist",
"Paste your gist url here...": "Dán liên kết gist vào đây ...",
"Import from Snippet": "Thêm từ Snippet",
"Select From Available Projects": "Chọn từ Project có sẵn",
"Select From Available Snippets": "Chọn từ Snippets có sẵn",
"OR": "HOẶC",
"Export to Snippet": "Xuất ra Snippet",
"Select Visibility Level": "Chọn cấp độ hiển thị",
"Night Theme": "Giao diện tối",
"Follow us on %s and %s.": "Cho phép chúng tôi %s, và %s.",
"Privacy": "Quyền riêng tư.",
"Terms of Use": "Điều khoản sử dụng.",
"Do you really want to delete your user account?": "Bạn có thực sự muốn xóa tài khoản ?",
"This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Điều này sẽ xóa tài khoản của bạn, tất cả các ghi chú thuộc sở hữu của bạn và xóa tất cả các liên kết đến tài khoản của bạn khỏi các ghi chú khác.",
"Delete user": "Xóa người dùng",
"Export user data": "Xuất dữ liệu người dùng",
"Help us translating on %s": "Giúp chúng tôi dịch trên %s",
"Source Code": "Mã nguồn",
"Register": "Đăng ký",
"Powered by %s": "Cung cấp bởi %s",
"Help us translating": "Giúp chúng tôi dịch",
"Join the community": "Tham gia vào cộng đồng"
}

View file

@ -1,13 +1,14 @@
{
"name": "CodiMD",
"version": "1.3.2",
"version": "1.4.0",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
"license": "AGPL-3.0",
"scripts": {
"test": "npm run-script eslint && npm run-script jsonlint && mocha",
"eslint": "node_modules/.bin/eslint lib public test app.js",
"test": "npm run-script eslint && npm run-script jsonlint && npm run-script mocha-suite",
"eslint": "node_modules/.bin/eslint --max-warnings 0 lib public test app.js",
"jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done",
"mocha-suite": "NODE_ENV=test CMD_DB_URL=\"sqlite::memory:\" mocha --exit",
"standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1",
"dev": "webpack --config webpack.dev.js --progress --colors --watch",
"heroku-prebuild": "bin/heroku",
@ -15,7 +16,6 @@
"start": "sequelize db:migrate && node app.js"
},
"dependencies": {
"@hackmd/js-sequence-diagrams": "^0.0.1-alpha.2",
"@passport-next/passport-openid": "^1.0.0",
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js",
"archiver": "^2.1.1",
@ -53,7 +53,7 @@
"i18n": "^0.8.3",
"imgur": "git+https://github.com/hackmdio/node-imgur.git",
"ionicons": "~2.0.1",
"jquery": "^3.1.1",
"jquery": "^3.4.1",
"jquery-mousewheel": "^3.1.13",
"jquery-ui": "^1.12.1",
"js-cookie": "^2.1.3",
@ -82,7 +82,7 @@
"markdown-pdf": "^9.0.0",
"mathjax": "~2.7.0",
"mattermost": "^3.4.0",
"mermaid": "~8.0.0",
"mermaid": "~7.1.0",
"meta-marked": "git+https://github.com/codimd/meta-marked#semver:^0.4.2",
"method-override": "^2.3.7",
"minimist": "^1.2.0",
@ -111,7 +111,8 @@
"readline-sync": "^1.4.7",
"request": "^2.88.0",
"reveal.js": "~3.7.0",
"scrypt": "^6.0.3",
"scrypt-async": "^2.0.1",
"scrypt-kdf": "^2.0.1",
"select2": "^3.5.2-browserify",
"sequelize": "^3.28.0",
"sequelize-cli": "^2.5.1",
@ -119,7 +120,7 @@
"socket.io": "~2.1.1",
"socket.io-client": "~2.1.1",
"spin.js": "^2.3.2",
"sqlite3": "^4.0.1",
"sqlite3": "^4.0.7",
"store": "^2.0.12",
"string": "^3.3.3",
"tedious": "^1.14.0",
@ -132,6 +133,7 @@
"viz.js": "^1.7.0",
"winston": "^3.1.0",
"ws": "^6.0.0",
"wurl": "^2.5.3",
"xss": "^1.0.3"
},
"resolutions": {
@ -140,7 +142,7 @@
"**/request": "^2.88.0"
},
"engines": {
"node": ">=6.x"
"node": ">=8.x"
},
"bugs": "https://github.com/codimd/server/issues",
"keywords": [

View file

@ -384,6 +384,24 @@ small .dropdown a:focus, small .dropdown a:hover {
color: #eee;
}
/* Prevent linked heading from being hidden underneath navbar.
* Example: http://localhost:3000/features#Editor-Modes would open and
* hide the headline "Editor Modes" underneath the navbar without this CSS rule.
*/
.markdown-body h1[id]:before,
.markdown-body h2[id]:before,
.markdown-body h3[id]:before,
.markdown-body h4[id]:before,
.markdown-body h5[id]:before,
.markdown-body h6[id]:before {
display: block;
content: " ";
margin-top: -55px;
height: 55px;
visibility: hidden;
}
@media print {
div, table, img, pre, blockquote {
page-break-inside: avoid !important;

View file

@ -20,24 +20,6 @@ body.night{
background: #333 !important;
}
.toolbar {
background-color: #1c1c1e;
border: 1px solid #343434;
}
.toolbar > .btn-toolbar > .btn-group > .btn {
background-color: #1c1c1e;
padding: 5px;
font-size: 1em;
}
.toolbar > .btn-toolbar > .btn-group > .btn:hover {
background-color: #383a3e;
padding: 5px;
}
.CodeMirror {
font-family: "Source Code Pro", Consolas, monaco, monospace;
letter-spacing: 0.025em;
@ -124,7 +106,10 @@ body.night{
color: #78B2F2 !important;
}
.CodeMirror-sizer {
margin-bottom: 0px !important;
/* Make sure CodeMirror doesn't hide text under the status bar
* 26px is the height of the status bar.
*/
margin-bottom: 26px !important;
}
.CodeMirror-insert-match {
background: lawngreen;

View file

@ -56,3 +56,64 @@
height: 1.5em;
border: 3px solid #777;
}
.markdown-body.slides aside.notes {
display: none;
}
.markdown-body.slides ul, .markdown-body.slides ol {
display: inline-block;
text-align: left;
margin: 0 0 0 1em;
padding: 0;
}
.markdown-body.slides table {
width: 50%;
margin: 0 auto;
border-collapse: collapse;
border-spacing: 0;
display: table;
}
.markdown-body.slides table th, .markdown-body.slides table td {
text-align: left;
padding: 0.2em 0.5em 0.2em 0.5em;
border:none;
border-bottom: 1px solid;
}
.markdown-body.slides table tr {
border-top: 0;
background-color: #fff;
}
.markdown-body.slides table tr:nth-child(2n) {
background-color: inherit;
}
.markdown-body.slides table tbody tr:last-child th, .markdown-body.slides table tbody tr:last-child td {
border-bottom: none;
}
.markdown-body.slides h1, .markdown-body.slides h2 {
border-bottom: 0;
}
.night .markdown-body.slides h1,
.night .markdown-body.slides h2,
.night .markdown-body.slides h3,
.night .markdown-body.slides h4,
.night .markdown-body.slides h5,
.night .markdown-body.slides h6 {
color: black;
}
.markdown-body section > section:last-child {
margin-bottom: 1.5em !important;
}
/* slides previews get a black background, controlled by js */
.ui-view-area.black {
background-color: black !important;;
}

33
public/css/ui/toolbar.css Normal file
View file

@ -0,0 +1,33 @@
.toolbar {
background-color: #fafafa;
border: 1px solid #ededed;
}
.toolbar > .btn-toolbar > .btn-group > .btn {
background-color: #fafafa;
padding: 5px;
font-size: 1em;
color: #555;
}
.toolbar > .btn-toolbar > .btn-group > .btn:hover {
background-color: #e1e1e1;
padding: 5px;
}
body.night .toolbar {
background-color: #1c1c1e;
border: 1px solid #353538;
}
body.night .toolbar > .btn-toolbar > .btn-group > .btn {
background-color: #1c1c1e;
padding: 5px;
font-size: 1em;
color: #5EB7E0;
}
body.night .toolbar > .btn-toolbar > .btn-group > .btn:hover {
background-color: #37373b;
padding: 5px;
}

View file

@ -1,6 +1,79 @@
Release Notes
===
<i class="fa fa-tag"></i> 1.4.0 <i class="fa fa-clock-o"></i> 2019-05-31 00:00
---
### Announcements
* CodiMD now has a [Mastodon account](https://social.codimd.org/mastodon)
* CodiMD now has a [community forum](https://community.codimd.org)
* With CodiMD 1.4.0 we're dropping node 6 support. That version of node.js is discontinued and no longer receives any security updates. We would like to encourage you to upgrade node 8 or later. Node 8 will continue to be supported at least until its end-of-life in January 2020.
### Enhancements
* Use libravatar instead of Gravatar
* Fix language description capitalization
* Move upload button into the toolbar
* Clean up Heroku configurations
* Add new screenshot to README and index page
* Add link to community call to README
* Update languages (pl, sr, zh-CN, fr, it, ja, zh-TW, de, sv, es)
* Change edit link to `both` view
* Hide minio default ports
* Add missing passport-saml configuration
* Add lutim support
* Update dependencies
* Add documentation for keycloak
* Add tests for user model
* Add Mastodon link
* Add config for toobusy middleware
* Add vietnamese language
### Fixes
* Fix missing space in footer
* Fix various possible security vulnerabilities in dependencies
* Fix broken dependency js-sequence-diagrams
* Fix XSS in graphviz error message rendering
* Fix toolbar night mode
* Fix hidden header on scroll
* Fix missing pictures for OpenID
* Fix statusbar hiding text in edit view
### Refactors
* Refactor README and documentation
* Integrate the old wiki into documentation section
* Refactor headers on Features page
* Replace js-url with wurl
* Refactor scrypt integration
### Removals
* Remove sass-loader
### Contributors
* [Amolith](https://github.com/Amolith)
* CasperS (translator)
* Cedric.couralet (translator)
* [Claudius Coenen (ccoenen)](https://github.com/ccoenen)
* Daniel (translator)
* Deluxghost (translator)
* [Dylan Dervaux (Dylanderv)](https://github.com/Dylanderv)
* [Emmanuel Ormancey (nopap)](https://github.com/nopap)
* Grzegorz (translator)
* [Henrik Hüttemann (HerHde)](https://github.com/HerHde)
* Hồng (translator)
* [Mauricio Robayo (archemiro)](https://github.com/archemiro)
* [Max Wu (jackycute)](https://github.com/jackycute)
* [naimo](https://github.com/naimo)
* [Pedro Ferreira (pferreir)](https://github.com/pferreir)
* [Simon Fish (boardfish)](https://github.com/boardfish)
* [Stéphane Guillou (stragu)](https://github.com/stragu)
* Sylke Vicious (translator)
* [Thor77](https://github.com/Thor77)
* veracosta (translator)
* Vladan (translator)
* War (translator)
* Zhai233 (translator)
<i class="fa fa-tag"></i> 1.3.2 <i class="fa fa-clock-o"></i> 2019-03-28 00:00
---
@ -101,7 +174,7 @@ Release Notes
* Refactor handling of template variables
* Refactor linting to use eslint
### Removes
### Removals
* Remove no longer working Octicons
* Remove links to our old Gitter channel
* Remove unused library node-uuid

View file

@ -1,11 +1,6 @@
/* eslint-env browser, jquery */
/* global moment, serverurl */
require('./locale')
require('../css/cover.css')
require('../css/site.css')
import {
checkIfAuth,
clearLoginState,
@ -32,6 +27,11 @@ import { saveAs } from 'file-saver'
import List from 'list.js'
import S from 'string'
require('./locale')
require('../css/cover.css')
require('../css/site.css')
const options = {
valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'],
item: `<li class="col-xs-12 col-sm-6 col-md-6 col-lg-4">

View file

@ -1,6 +1,22 @@
/* eslint-env browser, jquery */
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
/* global moment, serverurl */
import Prism from 'prismjs'
import hljs from 'highlight.js'
import PDFObject from 'pdfobject'
import S from 'string'
import { saveAs } from 'file-saver'
import escapeHTML from 'escape-html'
import getUIElements from './lib/editor/ui-elements'
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
/* Defined regex markdown it plugins */
import Plugin from 'markdown-it-regexp'
require('prismjs/themes/prism.css')
require('prismjs/components/prism-wiki')
require('prismjs/components/prism-haskell')
@ -10,18 +26,9 @@ require('prismjs/components/prism-jsx')
require('prismjs/components/prism-makefile')
require('prismjs/components/prism-gherkin')
import Prism from 'prismjs'
import hljs from 'highlight.js'
import PDFObject from 'pdfobject'
import S from 'string'
import { saveAs } from 'file-saver'
import escapeHTML from 'escape-html'
require('./lib/common/login')
require('../vendor/md-toc')
var Viz = require('viz.js')
import getUIElements from './lib/editor/ui-elements'
const ui = getUIElements()
// auto update last change
@ -665,7 +672,6 @@ export function exportToHTML (view) {
dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
}
const html = template(context)
// console.log(html);
const blob = new Blob([html], {
type: 'text/html;charset=utf-8'
})
@ -935,9 +941,6 @@ function highlightRender (code, lang) {
return result.value
}
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
export let md = markdownit('default', {
html: true,
breaks: true,
@ -1035,9 +1038,6 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
}
/* Defined regex markdown it plugins */
import Plugin from 'markdown-it-regexp'
// youtube
const youtubePlugin = new Plugin(
// regexp to match

View file

@ -1,4 +1,5 @@
/* eslint-env browser, jquery */
/* eslint no-console: ["error", { allow: ["warn", "error", "debug"] }] */
/* global serverurl, moment */
import store from 'store'

View file

@ -1,16 +1,8 @@
/* eslint-env browser, jquery */
/* eslint no-console: ["error", { allow: ["warn", "error", "debug"] }] */
/* global CodeMirror, Cookies, moment, Spinner, Idle, serverurl,
key, Dropbox, ot, hex2rgb, Visibility */
require('../vendor/showup/showup')
require('../css/index.css')
require('../css/extra.css')
require('../css/slide-preview.css')
require('../css/site.css')
require('highlight.js/styles/github-gist.css')
import TurndownService from 'turndown'
import { saveAs } from 'file-saver'
@ -83,6 +75,15 @@ import getUIElements from './lib/editor/ui-elements'
import modeType from './lib/modeType'
import appState from './lib/appState'
require('../vendor/showup/showup')
require('../css/index.css')
require('../css/extra.css')
require('../css/slide-preview.css')
require('../css/site.css')
require('highlight.js/styles/github-gist.css')
var defaultTextHeight = 20
var viewportMargin = 20
var defaultEditorMode = 'gfm'
@ -1075,7 +1076,8 @@ ui.modal.revision.on('show.bs.modal', function (e) {
})
.fail(function (err) {
if (debug) {
console.log(err)
// eslint-disable-next-line no-console
console.debug(err)
}
})
.always(function () {
@ -1185,7 +1187,8 @@ function selectRevision (time) {
})
.fail(function (err) {
if (debug) {
console.log(err)
// eslint-disable-next-line no-console
console.debug(err)
}
})
.always(function () {
@ -1245,7 +1248,8 @@ ui.modal.snippetImportProjects.change(function () {
})
.fail(function (err) {
if (debug) {
console.log(err)
// eslint-disable-next-line no-console
console.debug(err)
}
})
.always(function () {
@ -1503,7 +1507,7 @@ function replaceAll (data) {
}
function importFromUrl (url) {
// console.log(url);
// console.debug(url);
if (!url) return
if (!isValidURL(url)) {
showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not a valid URL :(', '', '', false)
@ -1768,7 +1772,7 @@ var authorship = []
var authorMarks = {} // temp variable
var addTextMarkers = [] // temp variable
function updateInfo (data) {
// console.log(data);
// console.debug(data);
if (data.hasOwnProperty('createtime') && window.createtime !== data.createtime) {
window.createtime = data.createtime
updateLastChange()
@ -1993,7 +1997,7 @@ editorInstance.on('update', function () {
})
})
socket.on('check', function (data) {
// console.log(data);
// console.debug(data);
updateInfo(data)
})
socket.on('permission', function (data) {
@ -2002,7 +2006,7 @@ socket.on('permission', function (data) {
var permission = null
socket.on('refresh', function (data) {
// console.log(data);
// console.debug(data);
editorInstance.config.docmaxlength = data.docmaxlength
editor.setOption('maxLength', editorInstance.config.docmaxlength)
updateInfo(data)
@ -2740,6 +2744,7 @@ function updateViewInner () {
delete md.metaError
var rendered = md.render(value)
if (md.meta.type && md.meta.type === 'slide') {
ui.area.view.addClass('black')
var slideOptions = {
separator: '^(\r\n?|\n)---(\r\n?|\n)$',
verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
@ -2756,6 +2761,7 @@ function updateViewInner () {
if (lastMeta.type && lastMeta.type === 'slide') {
refreshView()
ui.area.markdown.removeClass('slides')
ui.area.view.removeClass('black')
appState.syncscroll = true
checkSyncToggle()
}
@ -2817,8 +2823,8 @@ function partialUpdate (src, tar, des) {
var rawSrc = cloneAndRemoveDataAttr(src[i])
var rawTar = cloneAndRemoveDataAttr(tar[i])
if (rawSrc.outerHTML !== rawTar.outerHTML) {
// console.log(rawSrc);
// console.log(rawTar);
// console.debug(rawSrc);
// console.debug(rawTar);
$(des[i]).replaceWith(src[i])
}
}
@ -2881,12 +2887,12 @@ function partialUpdate (src, tar, des) {
var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i])
if (rawTarStart && rawTarEnd && rawTarStart.outerHTML === rawTarEnd.outerHTML) { overlap++ } else { break }
}
if (debug) { console.log('overlap:' + overlap) }
if (debug) { console.debug('overlap:' + overlap) }
// show diff content
if (debug) {
console.log('start:' + start)
console.log('tarEnd:' + tarEnd)
console.log('srcEnd:' + srcEnd)
console.debug('start:' + start)
console.debug('tarEnd:' + tarEnd)
console.debug('srcEnd:' + srcEnd)
}
tarEnd += overlap
srcEnd += overlap
@ -2920,15 +2926,15 @@ function partialUpdate (src, tar, des) {
}
// add elements
if (debug) {
console.log('ADD ELEMENTS')
console.log(newElements.join('\n'))
console.debug('ADD ELEMENTS')
console.debug(newElements.join('\n'))
}
if (des[start]) { $(newElements.join('')).insertBefore(des[start]) } else { $(newElements.join('')).insertAfter(des[start - 1]) }
// remove elements
if (debug) { console.log('REMOVE ELEMENTS') }
if (debug) { console.debug('REMOVE ELEMENTS') }
for (let j = 0; j < removeElements.length; j++) {
if (debug) {
console.log(removeElements[j].outerHTML)
console.debug(removeElements[j].outerHTML)
}
if (removeElements[j]) { $(removeElements[j]).remove() }
}
@ -3043,7 +3049,7 @@ function checkAbove (method) {
text.push(editor.getLine(i))
}
text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch)
// console.log(text);
// console.debug(text);
return method(text)
}
@ -3055,7 +3061,7 @@ function checkBelow (method) {
text.push(editor.getLine(i))
}
text = editor.getLine(cursor.line).slice(cursor.ch) + '\n' + text.join('\n')
// console.log(text);
// console.debug(text);
return method(text)
}
@ -3160,7 +3166,7 @@ $(editor.getInputField())
text.push(editor.getLine(cursor.line - 1))
text.push(editor.getLine(cursor.line))
text = text.join('\n')
// console.log(text);
// console.debug(text);
if (text === '\n```') { editor.doc.cm.execCommand('goLineUp') }
},
context: function (text) {
@ -3192,7 +3198,7 @@ $(editor.getInputField())
text.push(editor.getLine(cursor.line - 1))
text.push(editor.getLine(cursor.line))
text = text.join('\n')
// console.log(text);
// console.debug(text);
if (text === '\n:::') { editor.doc.cm.execCommand('goLineUp') }
},
context: function (text) {

View file

@ -3,6 +3,8 @@ import config from './config'
import statusBarTemplate from './statusbar.html'
import toolBarTemplate from './toolbar.html'
import '../../../css/ui/toolbar.css'
/* config section */
const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault
const defaultEditorMode = 'gfm'
@ -219,8 +221,8 @@ export default class Editor {
makeComment.click(() => {
utils.insertText(this.editor, '> []')
})
uploadImage.bind('change', function (e) {
console.log("tiggered")
var files = e.target.files || e.dataTransfer.files
e.dataTransfer = {}
e.dataTransfer.files = files

View file

@ -1,49 +1,49 @@
<div class="toolbar">
<div class="btn-toolbar" role="toolbar" aria-label="Editor toolbar">
<div class="btn-group" role="group">
<a id="makeBold" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Bold">
<a id="makeBold" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Bold">
<i class="fa fa-bold fa-fw"></i>
</a>
<a id="makeItalic" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Italic">
<a id="makeItalic" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Italic">
<i class="fa fa-italic fa-fw"></i>
</a>
<a id="makeStrike" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Strikethrough">
<a id="makeStrike" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Strikethrough">
<i class="fa fa-strikethrough fa-fw"></i>
</a>
<a id="makeHeader" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Heading">
<a id="makeHeader" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Heading">
<i class="fa fa-h1 fa-fw">H</i>
</a>
<a id="makeCode" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Code">
<a id="makeCode" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Code">
<i class="fa fa-code fa-fw"></i>
</a>
<a id="makeQuote" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Quote">
<a id="makeQuote" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Quote">
<i class="fa fa-quote-right fa-fw"></i>
</a>
<a id="makeGenericList" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="List">
<a id="makeGenericList" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="List">
<i class="fa fa-list fa-fw"></i>
</a>
<a id="makeOrderedList" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Numbered List">
<a id="makeOrderedList" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Numbered List">
<i class="fa fa-list-ol fa-fw"></i>
</a>
<a id="makeCheckList" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Check List">
<a id="makeCheckList" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Check List">
<i class="fa fa-check-square fa-fw"></i>
</a>
<a id="makeLink" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Link">
<a id="makeLink" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Link">
<i class="fa fa-link fa-fw"></i>
</a>
<a id="makeImage" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Image">
<a id="makeImage" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Image">
<i class="fa fa-image fa-fw"></i>
</a>
<span id="uploadImage" class="btn btn-sm btn-dark btn-file ui-upload-image" title="Upload Image">
<i class="fa fa-upload fa-fw"></i><input type="file" accept="image/*" name="upload" multiple>
</span>
<a id="makeTable" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Table">
<a id="uploadImage" class="btn btn-sm btn-file ui-upload-image" title="Upload Image">
<i class="fa fa-upload fa-fw"></i><input type="file" accept="image/*" name="upload" multiple title="Upload Image">
</a>
<a id="makeTable" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Table">
<i class="fa fa-table fa-fw"></i>
</a>
<a id="makeLine" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Line">
<a id="makeLine" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Line">
<i class="fa fa-minus fa-fw"></i>
</a>
<a id="makeComment" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Comment">
<a id="makeComment" class="btn btn-sm text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Comment">
<i class="fa fa-comment fa-fw"></i>
</a>
</div>

View file

@ -51,7 +51,7 @@ export function insertText (cm, text, cursorEnd = 0) {
let cursor = cm.getCursor()
cm.replaceSelection(text, cursor, cursor)
cm.focus()
cm.setCursor({line: cursor.line, ch: cursor.ch + cursorEnd})
cm.setCursor({ line: cursor.line, ch: cursor.ch + cursorEnd })
}
export function insertLink (cm, isImage) {
@ -80,7 +80,7 @@ export function insertLink (cm, isImage) {
cm.setSelections(ranges)
} else {
cm.replaceRange(symbol + linkEnd, cursor, cursor)
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length})
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length })
}
}
cm.focus()
@ -88,8 +88,8 @@ export function insertLink (cm, isImage) {
export function insertHeader (cm) {
let cursor = cm.getCursor()
let startOfLine = {line: cursor.line, ch: 0}
let startOfLineText = cm.getRange(startOfLine, {line: cursor.line, ch: 1})
let startOfLine = { line: cursor.line, ch: 0 }
let startOfLineText = cm.getRange(startOfLine, { line: cursor.line, ch: 1 })
// See if it is already a header
if (startOfLineText === '#') {
cm.replaceRange('#', startOfLine, startOfLine)
@ -108,14 +108,14 @@ export function insertOnStartOfLines (cm, symbol) {
if (!range.empty()) {
const from = range.from()
const to = range.to()
let selection = cm.getRange({line: from.line, ch: 0}, to)
let selection = cm.getRange({ line: from.line, ch: 0 }, to)
selection = selection.replace(/\n/g, '\n' + symbol)
selection = symbol + selection
cm.replaceRange(selection, from, to)
} else {
cm.replaceRange(symbol, {line: cursor.line, ch: 0}, {line: cursor.line, ch: 0})
cm.replaceRange(symbol, { line: cursor.line, ch: 0 }, { line: cursor.line, ch: 0 })
}
}
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length})
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length })
cm.focus()
}

View file

@ -1,12 +1,6 @@
/* eslint-env browser, jquery */
/* global refreshView */
require('../css/extra.css')
require('../css/slide-preview.css')
require('../css/site.css')
require('highlight.js/styles/github-gist.css')
import {
autoLinkify,
deduplicatedHeaderId,
@ -24,6 +18,12 @@ import {
import { preventXSS } from './render'
require('../css/extra.css')
require('../css/slide-preview.css')
require('../css/site.css')
require('highlight.js/styles/github-gist.css')
const markdown = $('#doc.markdown-body')
const text = markdown.text()
const lastMeta = md.meta

View file

@ -1,12 +1,12 @@
/* eslint-env browser, jquery */
/* global serverurl, Reveal, RevealMarkdown */
require('../css/extra.css')
require('../css/site.css')
import { preventXSS } from './render'
import { md, updateLastChange, removeDOMEvents, finishView } from './extra'
require('../css/extra.css')
require('../css/site.css')
const body = preventXSS($('.slides').text())
window.createtime = window.lastchangeui.time.attr('data-createtime')

View file

@ -87,26 +87,6 @@
margin-right: 10px;
}
/* Light theme */
.btn-light {
color: #555;
background-color: rgba(0, 0, 0,.1);
}
.btn-light:hover {
color: #111;
background-color: rgba(0, 0, 0,.25);
}
/* Dark theme */
.btn-dark {
color: #fff;
background-color: rgba(0, 0, 0,.5);
}
.btn-dark:hover {
color: #fff;
background-color: rgba(0, 0, 0,.9);
}
/* Buttons displayed throughout the content */
.btn-showup {
position: relative;

View file

@ -1,7 +1,7 @@
<script src="<%= serverURL %>/js/mathjax-config-extra.js"></script>
<% if(useCDN) { %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js" integrity="sha256-PieqE0QdEDMppwXrTzSZQr6tWFX3W5KkyRVyF1zN3eg=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>

View file

@ -48,7 +48,7 @@
<div id="ui-toc-affix" class="ui-affix-toc ui-toc-dropdown unselectable hidden-print" data-spy="affix" style="top:17px;display:none;" {{{lang}}} {{{dir}}}>
{{{ui-toc-affix}}}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
<script>

View file

@ -77,7 +77,7 @@
</a>
</div>
<div class="col-md-4 inner">
<a href="<%- serverURL %>/features#Slide-Modee">
<a href="<%- serverURL %>/features#Slide-Mode">
<i class="fa fa-tv fa-3x"></i>
<h4><%= __('Support slide mode') %></h4>
</a>
@ -149,12 +149,13 @@
<option value="ko">한국어</option>
<option value="id">Bahasa Indonesia</option>
<option value="sr">Cрпски</option>
<option value="vi">Tiếng Việt</option>
</select>
<p>
<%- __('Powered by %s', '<a href="https://codimd.org">CodiMD</a>') %> | <a href="<%- serverURL %>/s/release-notes" target="_blank" rel="noopener"><%= __('Releases') %></a> | <a href="<%- sourceURL %>" target="_blank" rel="noopener"><%= __('Source Code') %></a><% if(privacyStatement) { %> | <a href="<%- serverURL %>/s/privacy" target="_blank" rel="noopener"><%= __('Privacy') %></a><% } %><% if(termsOfUse) { %> | <a href="<%- serverURL %>/s/terms-of-use" target="_blank" rel="noopener"><%= __('Terms of Use') %></a><% } %>
</p>
<h6 class="social-foot">
<%- __('Follow us on %s and %s.', '<a href="https://github.com/codimd/server" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub</a>, <a href="https://riot.im/app/#/room/#codimd:matrix.org" target="_blank" rel="noopener"><i class="fa fa-comments"></i> Riot</a>', '<a href="https://translate.codimd.org" target="_blank" rel="noopener"><i class="fa fa-globe"></i> POEditor</a>') %>
<%- __('Follow us on %s and %s.', '<a href="https://github.com/codimd/server" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub</a>, <a href="https://community.codimd.org" target="_blank" rel="noopener"><i class="fa fa-users" aria-hidden="true"></i> Discourse</a>, <a href="https://riot.im/app/#/room/#codimd:matrix.org" target="_blank" rel="noopener"><i class="fa fa-comments"></i> Riot</a>, <a href="https://social.codimd.org/mastodon" target="_blank" rel="noopener"><i class="fa fa-mastodon"></i> Mastodon</a>', '<a href="https://translate.codimd.org" target="_blank" rel="noopener"><i class="fa fa-globe"></i> POEditor</a>') %>
</h6>
</div>
</div>

View file

@ -1,5 +1,5 @@
<% if(useCDN) { %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js" integrity="sha256-WwTza96H3BgcQTfEfxX7MFaFc/dZA0QrPRKDRLdFHJo=" crossorigin="anonymous" defer></script>

View file

@ -73,7 +73,7 @@
</html>
<script src="<%= serverURL %>/js/mathjax-config-extra.js"></script>
<% if(useCDN) { %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>

View file

@ -90,7 +90,7 @@
<% if(useCDN) { %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0/lib/js/head.min.js" integrity="sha256-CTcwyen1cxIrm4hlqdxe0y7Hq6B0rpxAKLiXMD3dJv0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0/js/reveal.min.js" integrity="sha256-Xr6ZH+/kc7hDVReZLO5khBknteLqu5oen/xnSraXrVk=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.7.0/js-yaml.min.js" integrity="sha256-8PanqYAVOGlOct+i65R+HqibK3KPsXINnrSfxN+Y/J0=" crossorigin="anonymous" defer></script>

64
test/user.js Normal file
View file

@ -0,0 +1,64 @@
/* eslint-env node, mocha */
'use strict'
const assert = require('assert')
const models = require('../lib/models')
const User = models.User
describe('User Sequelize model', function () {
beforeEach(() => {
return models.sequelize.sync({ force: true })
})
it('stores a password hash on creation and verifies that password', function () {
const userData = {
password: 'test123'
}
const intentionallyInvalidPassword = 'stuff'
return User.create(userData).then(u => {
return Promise.all([
u.verifyPassword(userData.password).then(result => assert.strictEqual(result, true)),
u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false))
]).catch(e => assert.fail(e))
})
})
it('can cope with password stored in standard scrypt header format', function () {
const testKey = '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9'
const validPassword = 'test'
const intentionallyInvalidPassword = 'stuff'
const u = User.build()
u.setDataValue('password', testKey) // this circumvents the setter - which we don't need in this case!
return Promise.all([
u.verifyPassword(validPassword).then(result => assert.strictEqual(result, true)),
u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false))
]).catch(e => assert.fail(e))
})
it('deals with various characters correctly', function () {
const combinations = [
// ['correct password', 'scrypt syle hash']
['test', '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9'],
['ohai', '736372797074000e00000008000000010efec4e5ce6a5294491f1b1cccc38d3562f84844b9271aef635f8bc338cf4e0e0bac62ebb11379e85894c1f694e038fc39b087b4fdacd1280b50a7382d7ffbfc82f2190bef70d47708d2a94b75126294'],
['my secret pw', '736372797074000f0000000800000001ffb4cd10a1dfe9e64c1e5416fd6d55b390b6822e78b46fd1f963fe9f317a1e05f9c5fee15e1f618286f4e38b55364ae1e7dc295c9dc33ee0f5712e86afe37e5784ff9c7cf84cf0e631dd11f84f3621e7'],
['my secret pw', /* different hash! */ '736372797074000f0000000800000001f6083e9593365acd07550f7c72f19973fb7d52c3ef0a78026ff66c48ab14493843c642167b5e6b7f31927e8eeb912bc2639e41955fae15da5099998948cfeacd022f705624931c3b30104e6bb296b805'],
['i am so extremely long, it\'s not even funny. Wait, you\'re still reading?', '736372797074000f00000008000000012d205f7bb529bb3a8b8bb25f5ab46197c7e9baf1aad64cf5e7b2584c84748cacf5e60631d58d21cb51fa34ea93b517e2fe2eb722931db5a70ff5a1330d821288ee7380c4136369f064b71b191a785a5b']
]
const intentionallyInvalidPassword = 'stuff'
return Promise.all(combinations.map((combination, index) => {
const u = User.build()
u.setDataValue('password', combination[1])
return Promise.all([
u.verifyPassword(combination[0])
.then(result => assert.strictEqual(result, true, `password #${index} "${combination[0]}" should have been verified`)),
u.verifyPassword(intentionallyInvalidPassword)
.then(result => assert.strictEqual(result, false, `password #${index} "${combination[0]}" should NOT have been verified`))
])
})).catch(e => assert.fail(e))
})
})

View file

@ -392,18 +392,6 @@ module.exports = {
MiniCssExtractPlugin.loader,
'css-loader'
]
}, {
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
'sass-loader'
]
}, {
test: /\.less$/,
use: [

1293
yarn.lock

File diff suppressed because it is too large Load diff