diff --git a/.travis.yml b/.travis.yml index 52da97f7..14135e04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,6 @@ env: 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 diff --git a/README.md b/README.md index 47b39602..71f19545 100644 --- a/README.md +++ b/README.md @@ -5,349 +5,74 @@ CodiMD [![version][github-version-badge]][github-release-page] [![POEditor][poeditor-image]][poeditor-url] -CodiMD lets you create real-time collaborative markdown notes on all platforms. -Inspired by Hackpad, with more focus on speed and flexibility, and build from [HackMD](https://hackmd.io) source code. -Feel free to contribute. +CodiMD lets you collaborate in real-time with markdown. +Built on [HackMD](https://hackmd.io) source code, CodiMD lets you host and control your team's content with speed and ease. -Thanks for using! :smile: +## CodiMD - The Open Source HackMD +[HackMD](https://hackmd.io) helps developers write better documents and build active communities with open collaboration. +HackMD is built with one promise - **You own and control all your content**: +- You should be able to easily [download all your online content at once](https://hackmd.io/c/news/%2Fs%2Fr1cx3a3SE). +- Your content formatting should be portable as well. (That's why we choose [markdown](https://hackmd.io/features#Typography).) +- You should be able to control your content's presentation with HTML, [slide mode](https://hackmd.io/p/slide-example), or [book mode](https://hackmd.io/c/book-example/). - - -# Table of Contents +With the same promise of you owning your content, CodiMD is the free software version of [HackMD](https://hackmd.io), developed and opened source by the HackMD team with reduced features, so you can use CodiMD for your community and own your data. *(See the [origin of the name CodiMD](https://github.com/hackmdio/hackmd/issues/720).)* -- [HackMD CE became CodiMD](#hackmd-ce-became-codimd) -- [Browsers Requirement](#browsers-requirement) -- [Installation](#installation) - - [Getting started (Native install)](#getting-started-native-install) - - [Prerequisite](#prerequisite) - - [Instructions](#instructions) - - [Heroku Deployment](#heroku-deployment) - - [Kubernetes](#kubernetes) -- [Upgrade](#upgrade) - - [Native setup](#native-setup) -- [Configuration](#configuration) - - [Environment variables (will overwrite other server configs)](#environment-variables-will-overwrite-other-server-configs) - - [Application settings `config.json`](#application-settings-configjson) - - [Third-party integration API key settings](#third-party-integration-api-key-settings) - - [Third-party integration OAuth callback URLs](#third-party-integration-oauth-callback-urls) -- [Developer Notes](#developer-notes) - - [Structure](#structure) - - [Operational Transformation](#operational-transformation) -- [License](#license) +CodiMD is perfect for open communities, while HackMD emphasizes on permission and access controls for commercial use cases. - +HackMD team is committed to keep CodiMD open source. All contributions are welcome! -# HackMD CE became CodiMD +## Documentation +You would find all documentation here: [CodiMD Documentation](https://hackmd.io/c/codimd-documentation) -CodiMD was recently renamed from its former name was HackMD. CodiMD is the free software version of HackMD. HackMD EE, which is a SaaS (Software as a Service) product available at [hackmd.io](https://hackmd.io). +### Deployment +If you want to spin up an instance and start using immediately, see [Docker deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-documentation#Deployment). +If you want to contribute to the project, start with [manual deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment). -We decided to change the name to break the confusion between HackMD and CodiMD, formally known as HackMD CE, as it never was an open core project. +### Configuration +CodiMD is highly customizable, learn about all configuration options of networking, security, performance, resources, privilege, privacy, image storage, and authentication in [CodiMD Configuration](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration). -*For the whole renaming story, see the [related issue](https://github.com/hackmdio/hackmd/issues/720)* +### Upgrading and Migration +Upgrade CodiMD from previous version? See [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-upgrade) +Migrating from Etherpad? Follow [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-migration-etherpad) -# Browsers Requirement +### Developer +Join our contributor community! Start from deploying [CodiMD manually](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment), [connecting to your own database](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-db-connection), [learn about the project structure](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-project-structure), to [build your changes](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-webpack) with the help of webpack. -- ![Chrome](http://browserbadge.com/chrome/47/18px) Chrome >= 47, Chrome for Android >= 47 -- ![Safari](http://browserbadge.com/safari/9/18px) Safari >= 9, iOS Safari >= 8.4 -- ![Firefox](http://browserbadge.com/firefox/44/18px) Firefox >= 44 -- ![IE](http://browserbadge.com/ie/9/18px) IE >= 9, Edge >= 12 -- ![Opera](http://browserbadge.com/opera/34/18px) Opera >= 34, Opera Mini not supported +## Contribution and Discussion +All contributions are welcome! Even asking a question helps. + +| Project | Contribution Types | Contribution Venue | +| ------- | ------------------ | ------------------ | +|**CodiMD**|:couple: Community chat|[Gitter](https://gitter.im/hackmdio/hackmd)| +||:bug: Issues, bugs, and feature requests|[Issue tracker](https://github.com/hackmdio/codimd/issues)| +||:books: Improve documentation|[Documentations](https://hackmd.io/c/codimd-documentation)| +||:pencil: Translation|[POEditor](https://poeditor.com/join/project/q0nuPWyztp)| +||:coffee: Donation|[Buy us coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=KDGS4PREHX6QQ&lc=US&item_name=HackMD¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted)| +|**HackMD**|:question: Issues related to [HackMD](https://hackmd.io/)|[Issue tracker](https://github.com/hackmdio/hackmd-io-issues/issues)| +||:pencil2: Translation|[hackmd-locales](https://github.com/hackmdio/hackmd-locales/tree/master/locales)| + +## Browser Support + +CodiMD is a service that runs on Node.js, while users use the service through browsers. We support your users using the following browsers: +- ![Chrome](http://browserbadge.com/chrome/47/18px) + - Chrome >= 47 + - Chrome for Android >= 47 +- ![Safari](http://browserbadge.com/safari/9/18px) + - Safari >= 9 + - iOS Safari >= 8.4 +- ![Firefox](http://browserbadge.com/firefox/44/18px) + - Firefox >= 44 +- ![IE](http://browserbadge.com/ie/9/18px) + - IE >= 9 + - Edge >= 12 +- ![Opera](http://browserbadge.com/opera/34/18px) + - Opera >= 34 + - Opera Mini not supported - Android Browser >= 4.4 -# Installation - -## Getting started (Native install) - -### Prerequisite - -- Node.js 6.x or up (test up to 7.5.0) and <10.x -- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8` -- npm (and its dependencies, especially [uWebSockets](https://github.com/uWebSockets/uWebSockets#nodejs-developers), [node-gyp](https://github.com/nodejs/node-gyp#installation)) -- `libssl-dev` for building scrypt (see [here](https://github.com/ml1nk/node-scrypt/blob/master/README.md#installation-instructions) for further information) -- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM - -### Instructions - -1. Download a release and unzip or clone into a directory -2. Enter the directory and type `bin/setup`, which will install npm dependencies and create configs. The setup script is written in Bash, you would need bash as a prerequisite. -3. Setup the configs, see more below -4. Setup environment variables which will overwrite the configs -5. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development) -6. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string - For example: `postgres://username:password@localhost:5432/codimd` -7. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema -8. Run the server as you like (node, forever, pm2) - To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed]. -## Heroku Deployment - -You can quickly setup a sample Heroku CodiMD application by clicking the button below. - -[![Deploy on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hackmdio/codimd/tree/master) - -If you deploy it without the button, keep in mind to use the right buildpacks. For details check `app.json`. - -## Kubernetes - -To install use `helm install stable/hackmd`. - -For all further details, please check out the offical CodiMD [K8s helm chart](https://github.com/kubernetes/charts/tree/master/stable/hackmd). - -**Debian-based version:** - -[![latest](https://images.microbadger.com/badges/version/hackmdio/hackmd:latest.svg)](https://microbadger.com/images/hackmdio/hackmd "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/hackmdio/hackmd:latest.svg)](https://microbadger.com/images/hackmdio/hackmd "Get your own image badge on microbadger.com") - -The easiest way to setup CodiMD using docker are using the following three commands: - -```console -git clone https://github.com/hackmdio/docker-hackmd.git -cd docker-hackmd -docker-compose up -``` -Read more about it in the [container repository…](https://github.com/hackmdio/docker-hackmd) - -# Upgrade - -## Native setup - -If you are upgrading CodiMD from an older version, follow these steps: - -1. Fully stop your old server first (important) -2. `git pull` or do whatever that updates the files -3. `npm install` to update dependencies -4. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development) -5. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string - For example: `postgres://username:password@localhost:5432/codimd` -6. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema -7. Start your whole new server! - -To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed]. - -* **migrate-to-1.1.0** - -We deprecated the older lower case config style and moved on to camel case style. Please have a look at the current `config.json.example` and check the warnings on startup. - -*Notice: This is not a breaking change right now but in the future* - -* [**migration-to-0.5.0**](https://github.com/hackmdio/migration-to-0.5.0) - -We don't use LZString to compress socket.io data and DB data after version 0.5.0. -Please run the migration tool if you're upgrading from the old version. - -* [**migration-to-0.4.0**](https://github.com/hackmdio/migration-to-0.4.0) - -We've dropped MongoDB after version 0.4.0. -So here is the migration tool for you to transfer the old DB data to the new DB. -This tool is also used for official service. - -# Configuration - -There are some config settings you need to change in the files below. - -``` -./config.json ----application settings -``` - -## Environment variables (will overwrite other server configs) - -| variables | example values | description | -| --------- | ------ | ----------- | -| `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) | -| `DEBUG` | `true` or `false` | set debug mode; show more logs | -| `CMD_CONFIG_FILE` | `/path/to/config.json` | optional override for the path to CodiMD's config file | -| `CMD_DOMAIN` | `codimd.org` | domain name | -| `CMD_URL_PATH` | `codimd` | sub URL path, like `www.example.com/` | -| `CMD_HOST` | `localhost` | host to listen on | -| `CMD_PORT` | `80` | web app port | -| `CMD_PATH` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `CMD_HOST` and `CMD_PORT` are ignored) | -| `CMD_LOGLEVEL` | `info` | Defines what kind of logs are provided to stdout. | -| `CMD_ALLOW_ORIGIN` | `localhost, codimd.org` | domain name whitelist (use comma to separate) | -| `CMD_PROTOCOL_USESSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) | -| `CMD_URL_ADDPORT` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | -| `CMD_USECDN` | `true` or `false` | set to use CDN resources or not (default is `true`) | -| `CMD_ALLOW_ANONYMOUS` | `true` or `false` | set to allow anonymous usage (default is `true`) | -| `CMD_ALLOW_ANONYMOUS_EDITS` | `true` or `false` | if `allowAnonymous` is `true`, allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) | -| `CMD_ALLOW_FREEURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | -| `CMD_FORBIDDEN_NODE_IDS` | `'robots.txt'` | disallow creation of notes, even if `CMD_ALLOW_FREEURL` is `true` | -| `CMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) | -| `CMD_DB_URL` | `mysql://localhost:3306/database` | set the database URL | -| `CMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If non is set, one will randomly generated on startup | -| `CMD_SESSION_LIFE` | `1209600000` | Session life time. (milliseconds) | -| `CMD_FACEBOOK_CLIENTID` | no example | Facebook API client id | -| `CMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret | -| `CMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key | -| `CMD_TWITTER_CONSUMERSECRET` | no example | Twitter API consumer secret | -| `CMD_GITHUB_CLIENTID` | no example | GitHub API client id | -| `CMD_GITHUB_CLIENTSECRET` | no example | GitHub API client secret | -| `CMD_GITLAB_SCOPE` | `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) | -| `CMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) | -| `CMD_GITLAB_CLIENTID` | no example | GitLab API client id | -| `CMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret | -| `CMD_GITLAB_VERSION` | no example | GitLab API version (v3 or v4) | -| `CMD_MATTERMOST_BASEURL` | no example | Mattermost authentication endpoint for versions below 5.0. For Mattermost version 5.0 and above, see [guide](docs/guides/auth/mattermost-self-hosted.md). | -| `CMD_MATTERMOST_CLIENTID` | no example | Mattermost API client id | -| `CMD_MATTERMOST_CLIENTSECRET` | no example | Mattermost API client secret | -| `CMD_DROPBOX_CLIENTID` | no example | Dropbox API client id | -| `CMD_DROPBOX_CLIENTSECRET` | no example | Dropbox API client secret | -| `CMD_GOOGLE_CLIENTID` | no example | Google API client id | -| `CMD_GOOGLE_CLIENTSECRET` | no example | Google API client secret | -| `CMD_LDAP_URL` | `ldap://example.com` | URL of LDAP server | -| `CMD_LDAP_BINDDN` | no example | bindDn for LDAP access | -| `CMD_LDAP_BINDCREDENTIALS` | no example | bindCredentials for LDAP access | -| `CMD_LDAP_SEARCHBASE` | `o=users,dc=example,dc=com` | LDAP directory to begin search from | -| `CMD_LDAP_SEARCHFILTER` | `(uid={{username}})` | LDAP filter to search with | -| `CMD_LDAP_SEARCHATTRIBUTES` | `displayName, mail` | LDAP attributes to search with (use comma to separate) | -| `CMD_LDAP_USERIDFIELD` | `uidNumber` or `uid` or `sAMAccountName` | The LDAP field which is used uniquely identify a user on CodiMD | -| `CMD_LDAP_USERNAMEFIELD` | Fallback to userid | The LDAP field which is used as the username on CodiMD | -| `CMD_LDAP_TLS_CA` | `server-cert.pem, root.pem` | Root CA for LDAP TLS in PEM format (use comma to separate) | -| `CMD_LDAP_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the LDAP provider | -| `CMD_SAML_IDPSSOURL` | `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](docs/guides/auth/saml-onelogin.md). | -| `CMD_SAML_IDPCERT` | `/path/to/cert.pem` | certificate file path of IdP in PEM format | -| `CMD_SAML_ISSUER` | no example | identity of the service provider (optional, default: serverurl)" | -| `CMD_SAML_IDENTIFIERFORMAT` | no example | name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) | -| `CMD_SAML_GROUPATTRIBUTE` | `memberOf` | attribute name for group list (optional) | -| `CMD_SAML_REQUIREDGROUPS` | `Hackmd-users` | group names that allowed (use vertical bar to separate) (optional) | -| `CMD_SAML_EXTERNALGROUPS` | `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) | -| `CMD_SAML_ATTRIBUTE_ID` | `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) | -| `CMD_SAML_ATTRIBUTE_USERNAME` | `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) | -| `CMD_SAML_ATTRIBUTE_EMAIL` | `mail` | attribute map for `email` (optional, default: NameID of SAML response if `CMD_SAML_IDENTIFIERFORMAT` is default) | -| `CMD_OAUTH2_USER_PROFILE_URL` | `https://example.com` | where retrieve information about a user after succesful login. Needs to output JSON. (no default value) Refer to the [Mattermost](docs/guides/auth/mattermost-self-hosted.md) or [Nextcloud](docs/guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. | -| `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | `name` | where to find the username in the JSON from the user profile URL. (no default value)| -| `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | `display-name` | where to find the display-name in the JSON from the user profile URL. (no default value) | -| `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | `email` | where to find the email address in the JSON from the user profile URL. (no default value) | -| `CMD_OAUTH2_TOKEN_URL` | `https://example.com` | sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) | -| `CMD_OAUTH2_AUTHORIZATION_URL` | `https://example.com` | authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) | -| `CMD_OAUTH2_CLIENT_ID` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) | -| `CMD_OAUTH2_CLIENT_SECRET` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) | -| `CMD_OAUTH2_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the oAuth2 provider | -| `CMD_IMGUR_CLIENTID` | no example | Imgur API client id | -| `CMD_EMAIL` | `true` or `false` | set to allow email signin | -| `CMD_ALLOW_PDF_EXPORT` | `true` or `false` | Enable or disable PDF exports | -| `CMD_ALLOW_EMAIL_REGISTER` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) | -| `CMD_ALLOW_GRAVATAR` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance | -| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md) | -| `CMD_S3_ACCESS_KEY_ID` | no example | AWS access key id | -| `CMD_S3_SECRET_ACCESS_KEY` | no example | AWS secret key | -| `CMD_S3_REGION` | `ap-northeast-1` | AWS S3 region | -| `CMD_S3_BUCKET` | no example | AWS S3 bucket name | -| `CMD_MINIO_ACCESS_KEY` | no example | Minio access key | -| `CMD_MINIO_SECRET_KEY` | no example | Minio secret key | -| `CMD_MINIO_ENDPOINT` | `minio.example.org` | Address of your Minio endpoint/instance | -| `CMD_MINIO_PORT` | `9000` | Port that is used for your Minio instance | -| `CMD_MINIO_SECURE` | `true` | If set to `true` HTTPS is used for Minio | -| `CMD_AZURE_CONNECTION_STRING` | no example | Azure Blob Storage connection string | -| `CMD_AZURE_CONTAINER` | no example | Azure Blob Storage container name (automatically created if non existent) | -| `CMD_HSTS_ENABLE` | ` true` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | -| `CMD_HSTS_INCLUDE_SUBDOMAINS` | `true` | set to include subdomains in HSTS (default is `true`) | -| `CMD_HSTS_MAX_AGE` | `31536000` | max duration in seconds to tell clients to keep HSTS status (default is a year) | -| `CMD_HSTS_PRELOAD` | `true` | whether to allow preloading of the site's HSTS status (e.g. into browsers) | -| `CMD_CSP_ENABLE` | `true` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | -| `CMD_CSP_REPORTURI` | `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | -| `CMD_SOURCE_URL` | `https://github.com/hackmdio/codimd/tree/` | 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) | - -***Note:** Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue to work.* - -## Application settings `config.json` - -| variables | example values | description | -| --------- | ------ | ----------- | -| `debug` | `true` or `false` | set debug mode, show more logs | -| `domain` | `localhost` | domain name | -| `urlPath` | `codimd` | sub URL path, like `www.example.com/` | -| `host` | `localhost` | host to listen on | -| `port` | `80` | web app port | -| `path` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `host` and `port` are ignored) | -| `loglevel` | `info` | Defines what kind of logs are provided to stdout. | -| `allowOrigin` | `['localhost']` | domain name whitelist | -| `useSSL` | `true` or `false` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) | -| `hsts` | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | -| `csp` | `{"enable": true, "directives": {"scriptSrc": "trustworthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": true}` | Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. Some defaults are added to the configured values so that the application doesn't break. To disable this behaviour, set `addDefaults` to `false`. Further, if `usecdn` is on, some CDN locations are allowed too. By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set `upgradeInsecureRequests` to either `true` or `false`. | -| `protocolUseSSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) | -| `urlAddPort` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | -| `useCDN` | `true` or `false` | set to use CDN resources or not (default is `true`) | -| `allowAnonymous` | `true` or `false` | set to allow anonymous usage (default is `true`) | -| `allowAnonymousEdits` | `true` or `false` | if `allowAnonymous` is `true`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) | -| `allowFreeURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | -| `forbiddenNoteIDs` | `['robots.txt']` | disallow creation of notes, even if `allowFreeUrl` is `true` | -| `defaultPermission` | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | set notes default permission (only applied on signed users) | -| `dbURL` | `mysql://localhost:3306/database` | set the db URL; if set, then db config (below) won't be applied | -| `db` | `{ "dialect": "sqlite", "storage": "./db.codimd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) | -| `sslKeyPath` | `./cert/client.key` | SSL key path1 (only need when you set `useSSL`) | -| `sslCertPath` | `./cert/codimd_io.crt` | SSL cert path1 (only need when you set `useSSL`) | -| `sslCAPath` | `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain1 (only need when you set `useSSL`) | -| `dhParamPath` | `./cert/dhparam.pem` | SSL dhparam path1 (only need when you set `useSSL`) | -| `tmpPath` | `./tmp/` | temp directory path1 | -| `defaultNotePath` | `./public/default.md` | default note file path1 | -| `docsPath` | `./public/docs` | docs directory path1 | -| `viewPath` | `./public/views` | template directory path1 | -| `uploadsPath` | `./public/uploads` | uploads directory1 - needs to be persistent when you use imageUploadType `filesystem` | -| `sessionName` | `connect.sid` | cookie session name | -| `sessionSecret` | `secret` | cookie session secret | -| `sessionLife` | `14 * 24 * 60 * 60 * 1000` | cookie session life | -| `staticCacheTime` | `1 * 24 * 60 * 60 * 1000` | static file cache time | -| `heartbeatInterval` | `5000` | socket.io heartbeat interval | -| `heartbeatTimeout` | `10000` | socket.io heartbeat timeout | -| `documentMaxLength` | `100000` | note max length | -| `email` | `true` or `false` | set to allow email signin | -| `oauth2` | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](docs/guides/auth/mattermost-self-hosted.md) or [Nextcloud](docs/guides/auth/nextcloud.md) examples for more details!| -| `allowEmailRegister` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) | -| `allowGravatar` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance | -| `imageUploadType` | `imgur`, `s3`, `minio`, `azure` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md)| -| `minio` | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also checkout our [Minio Image Upload Guide](docs/guides/minio-image-upload.md) | -| `s3` | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) | -| `s3bucket` | `YOUR_S3_BUCKET_NAME` | bucket name when `imageUploadType` is set to `s3` or `minio` | -| `sourceURL` | `https://github.com/hackmdio/codimd/tree/` | 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) | - -1: relative paths are based on CodiMD's base directory - -## Third-party integration API key settings - -| service | settings location | description | -| ------- | --------- | ----------- | -| facebook, twitter, github, gitlab, mattermost, dropbox, google, ldap, saml | environment variables or `config.json` | for signin | -| imgur, s3, minio, azure | environment variables or `config.json` | for image upload | -| dropbox(`dropbox/appKey`) | `config.json` | for export and import | - -## Third-party integration OAuth callback URLs - -| service | callback URL (after the server URL) | -| ------- | --------- | -| facebook | `/auth/facebook/callback` | -| twitter | `/auth/twitter/callback` | -| github | `/auth/github/callback` | -| gitlab | `/auth/gitlab/callback` | -| mattermost | `/auth/mattermost/callback` | -| dropbox | `/auth/dropbox/callback` | -| google | `/auth/google/callback` | -| saml | `/auth/saml/callback` | - -# Developer Notes - -## Structure - -```text -codimd/ -├── tmp/ --- temporary files -├── docs/ --- document files -├── lib/ --- server libraries -└── public/ --- client files - ├── css/ --- css styles - ├── js/ --- js scripts - ├── vendor/ --- vendor includes - └── views/ --- view templates -``` - -## Operational Transformation - -From 0.3.2, we started supporting operational transformation. -It makes concurrent editing safe and will not break up other users' operations. -Additionally, now can show other clients' selections. -See more at [http://operational-transformation.github.io/](http://operational-transformation.github.io/) - - - -# License +## License **License under AGPL.** diff --git a/bin/manage_users b/bin/manage_users index f744150d..0acbeeb1 100755 --- a/bin/manage_users +++ b/bin/manage_users @@ -1,119 +1,117 @@ #!/usr/bin/env node // First configure the logger so it does not spam the console -const logger = require("../lib/logger"); -logger.transports.forEach((transport) => transport.level = "warning") +const logger = require('../lib/logger') +logger.transports.forEach((transport) => { + transport.level = 'warning' +}) -const models = require("../lib/models/"); -const readline = require("readline-sync"); -const minimist = require("minimist"); +const models = require('../lib/models/') +const readline = require('readline-sync') +const minimist = require('minimist') -function showUsage(tips) { - console.log(`${tips} +function showUsage (tips) { + console.log(`${tips} Command-line utility to create users for email-signin. - Usage: bin/manage_users [--pass password] (--add | --del) user-email - Options: - --add Add user with the specified user-email - --del Delete user with specified user-email - --reset Reset user password with specified user-email - --pass Use password from cmdline rather than prompting -`); - process.exit(1); + Options: + --add\tAdd user with the specified user-email + --del\tDelete user with specified user-email + --reset\tReset user password with specified user-email + --pass\tUse password from cmdline rather than prompting +`) + process.exit(1) } -function getPass(argv, action) { - // Find whether we use cmdline or prompt password - if(typeof argv["pass"] !== 'string') { - return readline.question(`Password for ${argv[action]}:`, {hideEchoBack: true}); - } - console.log("Using password from commandline..."); - return argv["pass"]; +function getPass (argv, action) { + // Find whether we use cmdline or prompt password + if (typeof argv['pass'] !== 'string') { + return readline.question(`Password for ${argv[action]}:`, { hideEchoBack: true }) + } + console.log('Using password from commandline...') + return argv['pass'] } // Using an async function to be able to use await inside -async function createUser(argv) { - const existing_user = await models.User.findOne({where: {email: argv["add"]}}); - // Cannot create already-existing users - if(existing_user != undefined) { - console.log(`User with e-mail ${existing_user.email} already exists! Aborting ...`); - process.exit(1); - } +async function createUser (argv) { + const existingUser = await models.User.findOne({ where: { email: argv['add'] } }) + // Cannot create already-existing users + if (existingUser !== undefined) { + console.log(`User with e-mail ${existingUser.email} already exists! Aborting ...`) + process.exit(1) + } - const pass = getPass(argv, "add"); + const pass = getPass(argv, 'add') - - // Lets try to create, and check success - const ref = await models.User.create({email: argv["add"], password: pass}); - if(ref == undefined) { - console.log(`Could not create user with email ${argv["add"]}`); - process.exit(1); - } else - console.log(`Created user with email ${argv["add"]}`); + // Lets try to create, and check success + const ref = await models.User.create({ email: argv['add'], password: pass }) + if (ref === undefined) { + console.log(`Could not create user with email ${argv['add']}`) + process.exit(1) + } else { console.log(`Created user with email ${argv['add']}`) } } // Using an async function to be able to use await inside -async function deleteUser(argv) { - // Cannot delete non-existing users - const existing_user = await models.User.findOne({where: {email: argv["del"]}}); - if(existing_user === undefined) { - console.log(`User with e-mail ${argv["del"]} does not exist, cannot delete`); - process.exit(1); - } +async function deleteUser (argv) { + // Cannot delete non-existing users + const existingUser = await models.User.findOne({ where: { email: argv['del'] } }) + if (existingUser === undefined) { + console.log(`User with e-mail ${argv['del']} does not exist, cannot delete`) + process.exit(1) + } - // Sadly .destroy() does not return any success value with all - // backends. See sequelize #4124 - await existing_user.destroy(); - console.log(`Deleted user ${argv["del"]} ...`); + // Sadly .destroy() does not return any success value with all + // backends. See sequelize #4124 + await existingUser.destroy() + console.log(`Deleted user ${argv['del']} ...`) } - // Using an async function to be able to use await inside -async function resetUser(argv) { - const existing_user = await models.User.findOne({where: {email: argv["reset"]}}); - // Cannot reset non-existing users - if(existing_user == undefined) { - console.log(`User with e-mail ${argv["reset"]} does not exist, cannot reset`); - process.exit(1); - } +async function resetUser (argv) { + const existingUser = await models.User.findOne({ where: { email: argv['reset'] } }) + // Cannot reset non-existing users + if (existingUser === undefined) { + console.log(`User with e-mail ${argv['reset']} does not exist, cannot reset`) + process.exit(1) + } - const pass = getPass(argv, "reset"); + const pass = getPass(argv, 'reset') - // set password and save - existing_user.password = pass; - await existing_user.save(); - console.log(`User with email ${argv["reset"]} password has been reset`); + // set password and save + existingUser.password = pass + await existingUser.save() + console.log(`User with email ${argv['reset']} password has been reset`) } const options = { - add: createUser, - del: deleteUser, - reset: resetUser, -}; + add: createUser, + del: deleteUser, + reset: resetUser +} // Perform commandline-parsing -const argv = minimist(process.argv.slice(2)); +const argv = minimist(process.argv.slice(2)) -const keys = Object.keys(options); -const opts = keys.filter((key) => argv[key] !== undefined); -const action = opts[0]; +const keys = Object.keys(options) +const opts = keys.filter((key) => argv[key] !== undefined) +const action = opts[0] // Check for options missing if (opts.length === 0) { - showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`); + showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`) } // Check if both are specified if (opts.length > 1) { - showUsage(`You cannot ${action.join(' and ')} at the same time!`); + showUsage(`You cannot ${action.join(' and ')} at the same time!`) } // Check if not string if (typeof argv[action] !== 'string') { - showUsage(`You must follow an email after --${action}`); + showUsage(`You must follow an email after --${action}`) } // Call respective processing functions -options[action](argv).then(function() { - process.exit(0); -}); +options[action](argv).then(function () { + process.exit(0) +}) diff --git a/lib/config/environment.js b/lib/config/environment.js index fc757cf1..06a65b51 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -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, diff --git a/lib/config/hackmdEnvironment.js b/lib/config/hackmdEnvironment.js index bc20e58a..fb16dbc2 100644 --- a/lib/config/hackmdEnvironment.js +++ b/lib/config/hackmdEnvironment.js @@ -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, diff --git a/lib/config/index.js b/lib/config/index.js index e5d02dc1..984dddc0 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -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) @@ -159,8 +159,8 @@ if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) { if (config.sessionSecret === 'secret') { logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.') config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number - .toString('hex') // convert to hexadecimal format - .slice(0, config.sessionSecretLen) // return required number of characters + .toString('hex') // convert to hexadecimal format + .slice(0, config.sessionSecretLen) // return required number of characters } // Validate upload upload providers diff --git a/lib/config/oldEnvironment.js b/lib/config/oldEnvironment.js index a3b13cb9..06047553 100644 --- a/lib/config/oldEnvironment.js +++ b/lib/config/oldEnvironment.js @@ -1,6 +1,6 @@ 'use strict' -const {toBooleanConfig} = require('./utils') +const { toBooleanConfig } = require('./utils') module.exports = { debug: toBooleanConfig(process.env.DEBUG), diff --git a/lib/history.js b/lib/history.js index 9c389bfa..b3ec7cbb 100644 --- a/lib/history.js +++ b/lib/history.js @@ -1,7 +1,7 @@ 'use strict' // history // external modules -var LZString = require('lz-string') +var LZString = require('@hackmd/lz-string') // core var config = require('./config') diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index a5dd8207..349e95b9 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -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://www.gravatar.com/avatar/' + hexDigest; + photo = 'https://www.gravatar.com/avatar/' + hexDigest if (big) { photo += '?s=400' } else { diff --git a/lib/logger.js b/lib/logger.js index 5ef1860a..6d4bf69a 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,5 +1,5 @@ 'use strict' -const {createLogger, format, transports} = require('winston') +const { createLogger, format, transports } = require('winston') const logger = createLogger({ level: 'debug', diff --git a/lib/migrations/20160112220142-note-add-lastchange.js b/lib/migrations/20160112220142-note-add-lastchange.js index 87e3ff19..a2d74a9c 100644 --- a/lib/migrations/20160112220142-note-add-lastchange.js +++ b/lib/migrations/20160112220142-note-add-lastchange.js @@ -18,8 +18,8 @@ module.exports = { down: function (queryInterface, Sequelize) { return queryInterface.removeColumn('Notes', 'lastchangeAt') - .then(function () { - return queryInterface.removeColumn('Notes', 'lastchangeuserId') - }) + .then(function () { + return queryInterface.removeColumn('Notes', 'lastchangeuserId') + }) } } diff --git a/lib/migrations/20171009121200-longtext-for-mysql.js b/lib/migrations/20171009121200-longtext-for-mysql.js index 2a7d0d3a..cb9fc8a5 100644 --- a/lib/migrations/20171009121200-longtext-for-mysql.js +++ b/lib/migrations/20171009121200-longtext-for-mysql.js @@ -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 }) } } diff --git a/lib/migrations/20180209120907-longtext-of-authorship.js b/lib/migrations/20180209120907-longtext-of-authorship.js index 4cca5444..c5d8a1ef 100644 --- a/lib/migrations/20180209120907-longtext-of-authorship.js +++ b/lib/migrations/20180209120907-longtext-of-authorship.js @@ -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 }) } } diff --git a/lib/migrations/20180306150303-fix-enum.js b/lib/migrations/20180306150303-fix-enum.js index 0ee58a94..433a9749 100644 --- a/lib/migrations/20180306150303-fix-enum.js +++ b/lib/migrations/20180306150303-fix-enum.js @@ -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') }) } } diff --git a/lib/models/author.js b/lib/models/author.js index 03f832a4..e65791cb 100644 --- a/lib/models/author.js +++ b/lib/models/author.js @@ -18,25 +18,25 @@ module.exports = function (sequelize, DataTypes) { unique: true, fields: ['noteId', 'userId'] } - ], - classMethods: { - associate: function (models) { - Author.belongsTo(models.Note, { - foreignKey: 'noteId', - as: 'note', - constraints: false, - onDelete: 'CASCADE', - hooks: true - }) - Author.belongsTo(models.User, { - foreignKey: 'userId', - as: 'user', - constraints: false, - onDelete: 'CASCADE', - hooks: true - }) - } - } + ] }) + + Author.associate = function (models) { + Author.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false, + onDelete: 'CASCADE', + hooks: true + }) + Author.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + constraints: false, + onDelete: 'CASCADE', + hooks: true + }) + } + return Author } diff --git a/lib/models/index.js b/lib/models/index.js index ef70475e..88c1b168 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -3,14 +3,16 @@ 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') var logger = require('../logger') var dbconfig = cloneDeep(config.db) -dbconfig.logging = config.debug ? logger.info : false +dbconfig.logging = config.debug ? (data) => { + logger.info(data) +} : false var sequelize = null @@ -39,13 +41,13 @@ sequelize.processData = processData var db = {} fs.readdirSync(__dirname) - .filter(function (file) { - return (file.indexOf('.') !== 0) && (file !== 'index.js') - }) - .forEach(function (file) { - var model = sequelize.import(path.join(__dirname, file)) - db[model.name] = model - }) + .filter(function (file) { + return (file.indexOf('.') !== 0) && (file !== 'index.js') + }) + .forEach(function (file) { + var model = sequelize.import(path.join(__dirname, file)) + db[model.name] = model + }) Object.keys(db).forEach(function (modelName) { if ('associate' in db[modelName]) { diff --git a/lib/models/note.js b/lib/models/note.js index 0e8dd4dd..6251945a 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -2,18 +2,19 @@ // external modules var fs = require('fs') var path = require('path') -var LZString = require('lz-string') +var LZString = require('@hackmd/lz-string') var base64url = require('base64url') var md = require('markdown-it')() -var metaMarked = require('meta-marked') +var metaMarked = require('@hackmd/meta-marked') var cheerio = require('cheerio') var shortId = require('shortid') var Sequelize = require('sequelize') var async = require('async') var moment = require('moment') -var DiffMatchPatch = require('diff-match-patch') +var DiffMatchPatch = require('@hackmd/diff-match-patch') var dmp = new DiffMatchPatch() -var S = require('string') + +const { stripTags } = require('../../utils/string') // core var config = require('../config') @@ -86,486 +87,492 @@ module.exports = function (sequelize, DataTypes) { } }, { paranoid: false, - classMethods: { - associate: function (models) { - Note.belongsTo(models.User, { - foreignKey: 'ownerId', - as: 'owner', - constraints: false, - onDelete: 'CASCADE', - hooks: true - }) - Note.belongsTo(models.User, { - foreignKey: 'lastchangeuserId', - as: 'lastchangeuser', - constraints: false - }) - Note.hasMany(models.Revision, { - foreignKey: 'noteId', - constraints: false - }) - Note.hasMany(models.Author, { - foreignKey: 'noteId', - as: 'authors', - constraints: false - }) - }, - checkFileExist: function (filePath) { - try { - return fs.statSync(filePath).isFile() - } catch (err) { - return false - } - }, - encodeNoteId: function (id) { - // remove dashes in UUID and encode in url-safe base64 - let str = id.replace(/-/g, '') - let hexStr = Buffer.from(str, 'hex') - return base64url.encode(hexStr) - }, - decodeNoteId: function (encodedId) { - // decode from url-safe base64 - let id = base64url.toBuffer(encodedId).toString('hex') - // add dashes between the UUID string parts - let idParts = [] - idParts.push(id.substr(0, 8)) - idParts.push(id.substr(8, 4)) - idParts.push(id.substr(12, 4)) - idParts.push(id.substr(16, 4)) - idParts.push(id.substr(20, 12)) - return idParts.join('-') - }, - checkNoteIdValid: function (id) { - var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i - var result = id.match(uuidRegex) - if (result && result.length === 1) { return true } else { return false } - }, - parseNoteId: function (noteId, callback) { - async.series({ - parseNoteIdByAlias: function (_callback) { - // try to parse note id by alias (e.g. doc) - Note.findOne({ - where: { - alias: noteId - } - }).then(function (note) { - if (note) { - let filePath = path.join(config.docsPath, noteId + '.md') - if (Note.checkFileExist(filePath)) { - // if doc in filesystem have newer modified time than last change time - // then will update the doc in db - var fsModifiedTime = moment(fs.statSync(filePath).mtime) - var dbModifiedTime = moment(note.lastchangeAt || note.createdAt) - var body = fs.readFileSync(filePath, 'utf8') - var contentLength = body.length - var title = Note.parseNoteTitle(body) - if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { - note.update({ - title: title, - content: body, - lastchangeAt: fsModifiedTime - }).then(function (note) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - if (err) return _callback(err, null) - // update authorship on after making revision of docs - var patch = dmp.patch_fromText(revision.patch) - var operations = Note.transformPatchToOperations(patch, contentLength) - var authorship = note.authorship - for (let i = 0; i < operations.length; i++) { - authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship) - } - note.update({ - authorship: authorship - }).then(function (note) { - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - }) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return callback(null, note.id) - } - } else { - return callback(null, note.id) - } - } else { - var filePath = path.join(config.docsPath, noteId + '.md') - if (Note.checkFileExist(filePath)) { - Note.create({ - alias: noteId, - owner: null, - permission: 'locked' - }).then(function (note) { - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return _callback(null, null) - } - } - }).catch(function (err) { - return _callback(err, null) - }) - }, - // parse note id by LZString is deprecated, here for compability - parseNoteIdByLZString: function (_callback) { - // Calculate minimal string length for an UUID that is encoded - // base64 encoded and optimize comparsion by using -1 - // this should make a lot of LZ-String parsing errors obsolete - // as we can assume that a nodeId that is 48 chars or longer is a - // noteID. - const base64UuidLength = ((4 * 36) / 3) - 1 - if (!(noteId.length > base64UuidLength)) { - return _callback(null, null) - } - // try to parse note id by LZString Base64 - try { - var id = LZString.decompressFromBase64(noteId) - if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } - } catch (err) { - if (err.message === 'Cannot read property \'charAt\' of undefined') { - logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.') - } else { - logger.error(err) - } - return _callback(null, null) - } - }, - parseNoteIdByBase64Url: function (_callback) { - // try to parse note id by base64url - try { - var id = Note.decodeNoteId(noteId) - if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } - } catch (err) { - logger.error(err) - return _callback(null, null) - } - }, - parseNoteIdByShortId: function (_callback) { - // try to parse note id by shortId - try { - if (shortId.isValid(noteId)) { - Note.findOne({ - where: { - shortid: noteId - } - }).then(function (note) { - if (!note) return _callback(null, null) - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return _callback(null, null) - } - } catch (err) { - return _callback(err, null) - } - } - }, function (err, result) { - if (err) { - logger.error(err) - return callback(err, null) - } - return callback(null, null) - }) - }, - parseNoteInfo: function (body) { - var parsed = Note.extractMeta(body) - var $ = cheerio.load(md.render(parsed.markdown)) - return { - title: Note.extractNoteTitle(parsed.meta, $), - tags: Note.extractNoteTags(parsed.meta, $) - } - }, - parseNoteTitle: function (body) { - var parsed = Note.extractMeta(body) - var $ = cheerio.load(md.render(parsed.markdown)) - return Note.extractNoteTitle(parsed.meta, $) - }, - extractNoteTitle: function (meta, $) { - var title = '' - if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { - title = meta.title - } else { - var h1s = $('h1') - if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s } - } - if (!title) title = 'Untitled' - return title - }, - generateDescription: function (markdown) { - return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ') - }, - decodeTitle: function (title) { - return title || 'Untitled' - }, - generateWebTitle: function (title) { - title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD' - return title - }, - extractNoteTags: function (meta, $) { - var tags = [] - var rawtags = [] - if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) { - var metaTags = ('' + meta.tags).split(',') - for (let i = 0; i < metaTags.length; i++) { - var text = metaTags[i].trim() - if (text) rawtags.push(text) - } - } else { - var h6s = $('h6') - h6s.each(function (key, value) { - if (/^tags/gmi.test($(value).text())) { - var codes = $(value).find('code') - for (let i = 0; i < codes.length; i++) { - var text = S($(codes[i]).text().trim()).stripTags().s - if (text) rawtags.push(text) - } - } - }) - } - for (let i = 0; i < rawtags.length; i++) { - var found = false - for (let j = 0; j < tags.length; j++) { - if (tags[j] === rawtags[i]) { - found = true - break - } - } - if (!found) { tags.push(rawtags[i]) } - } - return tags - }, - extractMeta: function (content) { - var obj = null - try { - obj = metaMarked(content) - if (!obj.markdown) obj.markdown = '' - if (!obj.meta) obj.meta = {} - } catch (err) { - obj = { - markdown: content, - meta: {} - } - } - return obj - }, - parseMeta: function (meta) { - var _meta = {} - if (meta) { - if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title } - if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description } - if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots } - if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA } - if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus } - if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions } - } - return _meta - }, - updateAuthorshipByOperation: function (operation, userId, authorships) { - var index = 0 - var timestamp = Date.now() - for (let i = 0; i < operation.length; i++) { - var op = operation[i] - if (ot.TextOperation.isRetain(op)) { - index += op - } else if (ot.TextOperation.isInsert(op)) { - let opStart = index - let opEnd = index + op.length - var inserted = false - // authorship format: [userId, startPos, endPos, createdAt, updatedAt] - if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]) - else { - for (let j = 0; j < authorships.length; j++) { - let authorship = authorships[j] - if (!inserted) { - let nextAuthorship = authorships[j + 1] || -1 - if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) { - if (authorship[1] < opStart && authorship[2] > opStart) { - // divide - let postLength = authorship[2] - opStart - authorship[2] = opStart - authorship[4] = timestamp - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) - authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]) - j += 2 - inserted = true - } else if (authorship[1] >= opStart) { - authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]) - j += 1 - inserted = true - } else if (authorship[2] <= opStart) { - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) - j += 1 - inserted = true - } - } - } - if (authorship[1] >= opStart) { - authorship[1] += op.length - authorship[2] += op.length - } - } - } - index += op.length - } else if (ot.TextOperation.isDelete(op)) { - let opStart = index - let opEnd = index - op - if (operation.length === 1) { - authorships = [] - } else if (authorships.length > 0) { - for (let j = 0; j < authorships.length; j++) { - let authorship = authorships[j] - if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { - authorships.splice(j, 1) - j -= 1 - } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { - authorship[2] += op - authorship[4] = timestamp - } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { - authorship[2] = opStart - authorship[4] = timestamp - } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { - authorship[1] = opEnd - authorship[4] = timestamp - } - if (authorship[1] >= opEnd) { - authorship[1] += op - authorship[2] += op - } - } - } - index += op - } - } - // merge - for (let j = 0; j < authorships.length; j++) { - let authorship = authorships[j] - for (let k = j + 1; k < authorships.length; k++) { - let nextAuthorship = authorships[k] - if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { - let minTimestamp = Math.min(authorship[3], nextAuthorship[3]) - let maxTimestamp = Math.max(authorship[3], nextAuthorship[3]) - authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]) - authorships.splice(k, 1) - j -= 1 - break - } - } - } - // clear - for (let j = 0; j < authorships.length; j++) { - let authorship = authorships[j] - if (!authorship[0]) { - authorships.splice(j, 1) - j -= 1 - } - } - return authorships - }, - transformPatchToOperations: function (patch, contentLength) { - var operations = [] - if (patch.length > 0) { - // calculate original content length - for (let j = patch.length - 1; j >= 0; j--) { - var p = patch[j] - for (let i = 0; i < p.diffs.length; i++) { - var diff = p.diffs[i] - switch (diff[0]) { - case 1: // insert - contentLength -= diff[1].length - break - case -1: // delete - contentLength += diff[1].length - break - } - } - } - // generate operations - var bias = 0 - var lengthBias = 0 - for (let j = 0; j < patch.length; j++) { - var operation = [] - let p = patch[j] - var currIndex = p.start1 - var currLength = contentLength - bias - for (let i = 0; i < p.diffs.length; i++) { - let diff = p.diffs[i] - switch (diff[0]) { - case 0: // retain - if (i === 0) { - // first - operation.push(currIndex + diff[1].length) - } else if (i !== p.diffs.length - 1) { - // mid - operation.push(diff[1].length) - } else { - // last - operation.push(currLength + lengthBias - currIndex) - } - currIndex += diff[1].length - break - case 1: // insert - operation.push(diff[1]) - lengthBias += diff[1].length - currIndex += diff[1].length - break - case -1: // delete - operation.push(-diff[1].length) - bias += diff[1].length - currIndex += diff[1].length - break - } - } - operations.push(operation) - } - } - return operations - } - }, hooks: { - beforeCreate: function (note, options, callback) { - // if no content specified then use default note - if (!note.content) { - var body = null - let filePath = null - if (!note.alias) { - filePath = config.defaultNotePath - } else { - filePath = path.join(config.docsPath, note.alias + '.md') - } - if (Note.checkFileExist(filePath)) { - var fsCreatedTime = moment(fs.statSync(filePath).ctime) - body = fs.readFileSync(filePath, 'utf8') - note.title = Note.parseNoteTitle(body) - note.content = body - if (filePath !== config.defaultNotePath) { - note.createdAt = fsCreatedTime + beforeCreate: function (note, options) { + return new Promise(function (resolve, reject) { + // if no content specified then use default note + if (!note.content) { + var body = null + let filePath = null + if (!note.alias) { + filePath = config.defaultNotePath + } else { + filePath = path.join(config.docsPath, note.alias + '.md') + } + if (Note.checkFileExist(filePath)) { + var fsCreatedTime = moment(fs.statSync(filePath).ctime) + body = fs.readFileSync(filePath, 'utf8') + note.title = Note.parseNoteTitle(body) + note.content = body + if (filePath !== config.defaultNotePath) { + note.createdAt = fsCreatedTime + } } } - } - // if no permission specified and have owner then give default permission in config, else default permission is freely - if (!note.permission) { - if (note.ownerId) { - note.permission = config.defaultPermission - } else { - note.permission = 'freely' + // if no permission specified and have owner then give default permission in config, else default permission is freely + if (!note.permission) { + if (note.ownerId) { + note.permission = config.defaultPermission + } else { + note.permission = 'freely' + } } - } - return callback(null, note) + return resolve(note) + }) }, afterCreate: function (note, options, callback) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - callback(err, note) + return new Promise(function (resolve, reject) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + if (err) { + return reject(err) + } + return resolve(note) + }) }) } } }) + Note.associate = function (models) { + Note.belongsTo(models.User, { + foreignKey: 'ownerId', + as: 'owner', + constraints: false, + onDelete: 'CASCADE', + hooks: true + }) + Note.belongsTo(models.User, { + foreignKey: 'lastchangeuserId', + as: 'lastchangeuser', + constraints: false + }) + Note.hasMany(models.Revision, { + foreignKey: 'noteId', + constraints: false + }) + Note.hasMany(models.Author, { + foreignKey: 'noteId', + as: 'authors', + constraints: false + }) + } + Note.checkFileExist = function (filePath) { + try { + return fs.statSync(filePath).isFile() + } catch (err) { + return false + } + } + Note.encodeNoteId = function (id) { + // remove dashes in UUID and encode in url-safe base64 + let str = id.replace(/-/g, '') + let hexStr = Buffer.from(str, 'hex') + return base64url.encode(hexStr) + } + Note.decodeNoteId = function (encodedId) { + // decode from url-safe base64 + let id = base64url.toBuffer(encodedId).toString('hex') + // add dashes between the UUID string parts + let idParts = [] + idParts.push(id.substr(0, 8)) + idParts.push(id.substr(8, 4)) + idParts.push(id.substr(12, 4)) + idParts.push(id.substr(16, 4)) + idParts.push(id.substr(20, 12)) + return idParts.join('-') + } + Note.checkNoteIdValid = function (id) { + var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + var result = id.match(uuidRegex) + if (result && result.length === 1) { return true } else { return false } + } + Note.parseNoteId = function (noteId, callback) { + async.series({ + parseNoteIdByAlias: function (_callback) { + // try to parse note id by alias (e.g. doc) + Note.findOne({ + where: { + alias: noteId + } + }).then(function (note) { + if (note) { + let filePath = path.join(config.docsPath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + // if doc in filesystem have newer modified time than last change time + // then will update the doc in db + var fsModifiedTime = moment(fs.statSync(filePath).mtime) + var dbModifiedTime = moment(note.lastchangeAt || note.createdAt) + var body = fs.readFileSync(filePath, 'utf8') + var contentLength = body.length + var title = Note.parseNoteTitle(body) + if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { + note.update({ + title: title, + content: body, + lastchangeAt: fsModifiedTime + }).then(function (note) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + if (err) return _callback(err, null) + // update authorship on after making revision of docs + var patch = dmp.patch_fromText(revision.patch) + var operations = Note.transformPatchToOperations(patch, contentLength) + var authorship = note.authorship + for (let i = 0; i < operations.length; i++) { + authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship) + } + note.update({ + authorship: authorship + }).then(function (note) { + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) + }) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return callback(null, note.id) + } + } else { + return callback(null, note.id) + } + } else { + var filePath = path.join(config.docsPath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + Note.create({ + alias: noteId, + owner: null, + permission: 'locked' + }).then(function (note) { + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return _callback(null, null) + } + } + }).catch(function (err) { + return _callback(err, null) + }) + }, + // parse note id by LZString is deprecated, here for compability + parseNoteIdByLZString: function (_callback) { + // Calculate minimal string length for an UUID that is encoded + // base64 encoded and optimize comparsion by using -1 + // this should make a lot of LZ-String parsing errors obsolete + // as we can assume that a nodeId that is 48 chars or longer is a + // noteID. + const base64UuidLength = ((4 * 36) / 3) - 1 + if (!(noteId.length > base64UuidLength)) { + return _callback(null, null) + } + // try to parse note id by LZString Base64 + try { + var id = LZString.decompressFromBase64(noteId) + if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } + } catch (err) { + if (err.message === 'Cannot read property \'charAt\' of undefined') { + logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.') + } else { + logger.error(err) + } + return _callback(null, null) + } + }, + parseNoteIdByBase64Url: function (_callback) { + // try to parse note id by base64url + try { + var id = Note.decodeNoteId(noteId) + if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } + } catch (err) { + logger.error(err) + return _callback(null, null) + } + }, + parseNoteIdByShortId: function (_callback) { + // try to parse note id by shortId + try { + if (shortId.isValid(noteId)) { + Note.findOne({ + where: { + shortid: noteId + } + }).then(function (note) { + if (!note) return _callback(null, null) + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return _callback(null, null) + } + } catch (err) { + return _callback(err, null) + } + } + }, function (err, result) { + if (err) { + logger.error(err) + return callback(err, null) + } + return callback(null, null) + }) + } + Note.parseNoteInfo = function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return { + title: Note.extractNoteTitle(parsed.meta, $), + tags: Note.extractNoteTags(parsed.meta, $) + } + } + Note.parseNoteTitle = function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return Note.extractNoteTitle(parsed.meta, $) + } + Note.extractNoteTitle = function (meta, $) { + var title = '' + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { + title = meta.title + } else { + var h1s = $('h1') + if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = stripTags(h1s.first().text()) } + } + if (!title) title = 'Untitled' + return title + } + Note.generateDescription = function (markdown) { + return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ') + } + Note.decodeTitle = function (title) { + return title || 'Untitled' + } + Note.generateWebTitle = function (title) { + title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD' + return title + } + Note.extractNoteTags = function (meta, $) { + var tags = [] + var rawtags = [] + if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) { + var metaTags = ('' + meta.tags).split(',') + for (let i = 0; i < metaTags.length; i++) { + var text = metaTags[i].trim() + if (text) rawtags.push(text) + } + } else { + var h6s = $('h6') + h6s.each(function (key, value) { + if (/^tags/gmi.test($(value).text())) { + var codes = $(value).find('code') + for (let i = 0; i < codes.length; i++) { + var text = stripTags($(codes[i]).text().trim()) + if (text) rawtags.push(text) + } + } + }) + } + for (let i = 0; i < rawtags.length; i++) { + var found = false + for (let j = 0; j < tags.length; j++) { + if (tags[j] === rawtags[i]) { + found = true + break + } + } + if (!found) { tags.push(rawtags[i]) } + } + return tags + } + Note.extractMeta = function (content) { + var obj = null + try { + obj = metaMarked(content) + if (!obj.markdown) obj.markdown = '' + if (!obj.meta) obj.meta = {} + } catch (err) { + obj = { + markdown: content, + meta: {} + } + } + return obj + } + Note.parseMeta = function (meta) { + var _meta = {} + if (meta) { + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title } + if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description } + if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots } + if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA } + if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus } + if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions } + } + return _meta + } + Note.updateAuthorshipByOperation = function (operation, userId, authorships) { + var index = 0 + var timestamp = Date.now() + for (let i = 0; i < operation.length; i++) { + var op = operation[i] + if (ot.TextOperation.isRetain(op)) { + index += op + } else if (ot.TextOperation.isInsert(op)) { + let opStart = index + let opEnd = index + op.length + var inserted = false + // authorship format: [userId, startPos, endPos, createdAt, updatedAt] + if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]) + else { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!inserted) { + let nextAuthorship = authorships[j + 1] || -1 + if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) { + if (authorship[1] < opStart && authorship[2] > opStart) { + // divide + let postLength = authorship[2] - opStart + authorship[2] = opStart + authorship[4] = timestamp + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]) + j += 2 + inserted = true + } else if (authorship[1] >= opStart) { + authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true + } else if (authorship[2] <= opStart) { + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true + } + } + } + if (authorship[1] >= opStart) { + authorship[1] += op.length + authorship[2] += op.length + } + } + } + index += op.length + } else if (ot.TextOperation.isDelete(op)) { + let opStart = index + let opEnd = index - op + if (operation.length === 1) { + authorships = [] + } else if (authorships.length > 0) { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { + authorships.splice(j, 1) + j -= 1 + } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { + authorship[2] += op + authorship[4] = timestamp + } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { + authorship[2] = opStart + authorship[4] = timestamp + } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { + authorship[1] = opEnd + authorship[4] = timestamp + } + if (authorship[1] >= opEnd) { + authorship[1] += op + authorship[2] += op + } + } + } + index += op + } + } + // merge + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + for (let k = j + 1; k < authorships.length; k++) { + let nextAuthorship = authorships[k] + if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { + let minTimestamp = Math.min(authorship[3], nextAuthorship[3]) + let maxTimestamp = Math.max(authorship[3], nextAuthorship[3]) + authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]) + authorships.splice(k, 1) + j -= 1 + break + } + } + } + // clear + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!authorship[0]) { + authorships.splice(j, 1) + j -= 1 + } + } + return authorships + } + Note.transformPatchToOperations = function (patch, contentLength) { + var operations = [] + if (patch.length > 0) { + // calculate original content length + for (let j = patch.length - 1; j >= 0; j--) { + var p = patch[j] + for (let i = 0; i < p.diffs.length; i++) { + var diff = p.diffs[i] + switch (diff[0]) { + case 1: // insert + contentLength -= diff[1].length + break + case -1: // delete + contentLength += diff[1].length + break + } + } + } + // generate operations + var bias = 0 + var lengthBias = 0 + for (let j = 0; j < patch.length; j++) { + var operation = [] + let p = patch[j] + var currIndex = p.start1 + var currLength = contentLength - bias + for (let i = 0; i < p.diffs.length; i++) { + let diff = p.diffs[i] + switch (diff[0]) { + case 0: // retain + if (i === 0) { + // first + operation.push(currIndex + diff[1].length) + } else if (i !== p.diffs.length - 1) { + // mid + operation.push(diff[1].length) + } else { + // last + operation.push(currLength + lengthBias - currIndex) + } + currIndex += diff[1].length + break + case 1: // insert + operation.push(diff[1]) + lengthBias += diff[1].length + currIndex += diff[1].length + break + case -1: // delete + operation.push(-diff[1].length) + bias += diff[1].length + currIndex += diff[1].length + break + } + } + operations.push(operation) + } + } + return operations + } + return Note } diff --git a/lib/models/revision.js b/lib/models/revision.js index 4ee080da..b628d96d 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -7,6 +7,8 @@ var childProcess = require('child_process') var shortId = require('shortid') var path = require('path') +var Op = Sequelize.Op + // core var config = require('../config') var logger = require('../logger') @@ -97,214 +99,212 @@ module.exports = function (sequelize, DataTypes) { this.setDataValue('authorship', value ? JSON.stringify(value) : value) } } - }, { - classMethods: { - associate: function (models) { - Revision.belongsTo(models.Note, { - foreignKey: 'noteId', - as: 'note', - constraints: false, - onDelete: 'CASCADE', - hooks: true - }) + }) + + Revision.associate = function (models) { + Revision.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false, + onDelete: 'CASCADE', + hooks: true + }) + } + Revision.getNoteRevisions = function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id }, - getNoteRevisions: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions) { - var data = [] - for (var i = 0, l = revisions.length; i < l; i++) { - var revision = revisions[i] - data.push({ - time: moment(revision.createdAt).valueOf(), - length: revision.length - }) + order: [['createdAt', 'DESC']] + }).then(function (revisions) { + var data = [] + for (var i = 0, l = revisions.length; i < l; i++) { + var revision = revisions[i] + data.push({ + time: moment(revision.createdAt).valueOf(), + length: revision.length + }) + } + callback(null, data) + }).catch(function (err) { + callback(err, null) + }) + } + Revision.getPatchedNoteRevisionByTime = function (note, time, callback) { + // find all revisions to prepare for all possible calculation + Revision.findAll({ + where: { + noteId: note.id + }, + order: [['createdAt', 'DESC']] + }).then(function (revisions) { + if (revisions.length <= 0) return callback(null, null) + // measure target revision position + Revision.count({ + where: { + noteId: note.id, + createdAt: { + [Op.gte]: time } - callback(null, data) - }).catch(function (err) { - callback(err, null) - }) - }, - getPatchedNoteRevisionByTime: function (note, time, callback) { - // find all revisions to prepare for all possible calculation - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions) { - if (revisions.length <= 0) return callback(null, null) - // measure target revision position - Revision.count({ - where: { - noteId: note.id, - createdAt: { - $gte: time + }, + order: [['createdAt', 'DESC']] + }).then(function (count) { + if (count <= 0) return callback(null, null) + sendDmpWorker({ + msg: 'get revision', + revisions: revisions, + count: count + }, callback) + }).catch(function (err) { + return callback(err, null) + }) + }).catch(function (err) { + return callback(err, null) + }) + } + Revision.checkAllNotesRevision = function (callback) { + Revision.saveAllNotesRevision(function (err, notes) { + if (err) return callback(err, null) + if (!notes || notes.length <= 0) { + return callback(null, notes) + } else { + Revision.checkAllNotesRevision(callback) + } + }) + } + Revision.saveAllNotesRevision = function (callback) { + sequelize.models.Note.findAll({ + // query all notes that need to save for revision + where: { + [Op.and]: [ + { + lastchangeAt: { + [Op.or]: { + [Op.eq]: null, + [Op.and]: { + [Op.ne]: null, + [Op.gt]: sequelize.col('createdAt') + } } - }, - order: [['createdAt', 'DESC']] - }).then(function (count) { - if (count <= 0) return callback(null, null) - sendDmpWorker({ - msg: 'get revision', - revisions: revisions, - count: count - }, callback) - }).catch(function (err) { - return callback(err, null) - }) - }).catch(function (err) { - return callback(err, null) - }) - }, - checkAllNotesRevision: function (callback) { - Revision.saveAllNotesRevision(function (err, notes) { - if (err) return callback(err, null) - if (!notes || notes.length <= 0) { - return callback(null, notes) + } + }, + { + savedAt: { + [Op.or]: { + [Op.eq]: null, + [Op.lt]: sequelize.col('lastchangeAt') + } + } + } + ] + } + }).then(function (notes) { + if (notes.length <= 0) return callback(null, notes) + var savedNotes = [] + async.each(notes, function (note, _callback) { + // revision saving policy: note not been modified for 5 mins or not save for 10 mins + if (note.lastchangeAt && note.savedAt) { + var lastchangeAt = moment(note.lastchangeAt) + var savedAt = moment(note.savedAt) + if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) } else { - Revision.checkAllNotesRevision(callback) + return _callback(null, null) } - }) + } else { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } + }, function (err) { + if (err) { + return callback(err, null) + } + // return null when no notes need saving at this moment but have delayed tasks to be done + var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes + return callback(null, result) + }) + }).catch(function (err) { + return callback(err, null) + }) + } + Revision.saveNoteRevision = function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id }, - saveAllNotesRevision: function (callback) { - sequelize.models.Note.findAll({ - // query all notes that need to save for revision - where: { - $and: [ - { - lastchangeAt: { - $or: { - $eq: null, - $and: { - $ne: null, - $gt: sequelize.col('createdAt') - } - } - } - }, - { - savedAt: { - $or: { - $eq: null, - $lt: sequelize.col('lastchangeAt') - } - } - } - ] - } - }).then(function (notes) { - if (notes.length <= 0) return callback(null, notes) - var savedNotes = [] - async.each(notes, function (note, _callback) { - // revision saving policy: note not been modified for 5 mins or not save for 10 mins - if (note.lastchangeAt && note.savedAt) { - var lastchangeAt = moment(note.lastchangeAt) - var savedAt = moment(note.savedAt) - if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } else { - return _callback(null, null) - } - } else { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } - }, function (err) { - if (err) { - return callback(err, null) - } - // return null when no notes need saving at this moment but have delayed tasks to be done - var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes - return callback(null, result) - }) + order: [['createdAt', 'DESC']] + }).then(function (revisions) { + if (revisions.length <= 0) { + // if no revision available + Revision.create({ + noteId: note.id, + lastContent: note.content ? note.content : '', + length: note.content ? note.content.length : 0, + authorship: note.authorship + }).then(function (revision) { + Revision.finishSaveNoteRevision(note, revision, callback) }).catch(function (err) { return callback(err, null) }) - }, - saveNoteRevision: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions) { - if (revisions.length <= 0) { - // if no revision available - Revision.create({ - noteId: note.id, - lastContent: note.content ? note.content : '', - length: note.content ? note.content.length : 0, - authorship: note.authorship + } else { + var latestRevision = revisions[0] + var lastContent = latestRevision.content || latestRevision.lastContent + var content = note.content + sendDmpWorker({ + msg: 'create patch', + lastDoc: lastContent, + currDoc: content + }, function (err, patch) { + if (err) logger.error('save note revision error', err) + if (!patch) { + // if patch is empty (means no difference) then just update the latest revision updated time + latestRevision.changed('updatedAt', true) + latestRevision.update({ + updatedAt: Date.now() }).then(function (revision) { Revision.finishSaveNoteRevision(note, revision, callback) }).catch(function (err) { return callback(err, null) }) } else { - var latestRevision = revisions[0] - var lastContent = latestRevision.content || latestRevision.lastContent - var content = note.content - sendDmpWorker({ - msg: 'create patch', - lastDoc: lastContent, - currDoc: content - }, function (err, patch) { - if (err) logger.error('save note revision error', err) - if (!patch) { - // if patch is empty (means no difference) then just update the latest revision updated time - latestRevision.changed('updatedAt', true) - latestRevision.update({ - updatedAt: Date.now() - }).then(function (revision) { - Revision.finishSaveNoteRevision(note, revision, callback) - }).catch(function (err) { - return callback(err, null) - }) - } else { - Revision.create({ - noteId: note.id, - patch: patch, - content: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision) { - // clear last revision content to reduce db size - latestRevision.update({ - content: null - }).then(function () { - Revision.finishSaveNoteRevision(note, revision, callback) - }).catch(function (err) { - return callback(err, null) - }) - }).catch(function (err) { - return callback(err, null) - }) - } + Revision.create({ + noteId: note.id, + patch: patch, + content: note.content, + length: note.content.length, + authorship: note.authorship + }).then(function (revision) { + // clear last revision content to reduce db size + latestRevision.update({ + content: null + }).then(function () { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) + }).catch(function (err) { + return callback(err, null) }) } - }).catch(function (err) { - return callback(err, null) - }) - }, - finishSaveNoteRevision: function (note, revision, callback) { - note.update({ - savedAt: revision.updatedAt - }).then(function () { - return callback(null, revision) - }).catch(function (err) { - return callback(err, null) }) } - } - }) + }).catch(function (err) { + return callback(err, null) + }) + } + Revision.finishSaveNoteRevision = function (note, revision, callback) { + note.update({ + savedAt: revision.updatedAt + }).then(function () { + return callback(null, revision) + }).catch(function (err) { + return callback(err, null) + }) + } return Revision } diff --git a/lib/models/user.js b/lib/models/user.js index 648db73e..ba817c0b 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,11 +1,11 @@ 'use strict' // external modules var Sequelize = require('sequelize') -var scrypt = require('@mlink/scrypt') +var scrypt = require('scrypt') // core var logger = require('../logger') -var {generateAvatarURL} = require('../letter-avatars') +var { generateAvatarURL } = require('../letter-avatars') module.exports = function (sequelize, DataTypes) { var User = sequelize.define('User', { @@ -47,111 +47,108 @@ module.exports = function (sequelize, DataTypes) { this.setDataValue('password', hash) } } - }, { - instanceMethods: { - verifyPassword: function (attempt) { - if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) { - return this - } else { - return false - } - } - }, - classMethods: { - associate: function (models) { - User.hasMany(models.Note, { - foreignKey: 'ownerId', - constraints: false - }) - User.hasMany(models.Note, { - foreignKey: 'lastchangeuserId', - constraints: false - }) - }, - getProfile: function (user) { - if (!user) { - return null - } - return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) - }, - parseProfile: function (profile) { - try { - profile = JSON.parse(profile) - } catch (err) { - logger.error(err) - profile = null - } - if (profile) { - profile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile), - biggerphoto: User.parsePhotoByProfile(profile, true) - } - } - return profile - }, - parsePhotoByProfile: function (profile, bigger) { - var photo = null - switch (profile.provider) { - case 'facebook': - photo = 'https://graph.facebook.com/' + profile.id + '/picture' - if (bigger) photo += '?width=400' - else photo += '?width=96' - break - case 'twitter': - photo = 'https://twitter.com/' + profile.username + '/profile_image' - if (bigger) photo += '?size=original' - else photo += '?size=bigger' - break - case 'github': - photo = 'https://avatars.githubusercontent.com/u/' + profile.id - if (bigger) photo += '?s=400' - else photo += '?s=96' - break - case 'gitlab': - photo = profile.avatarUrl - if (photo) { - if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') - else photo = photo.replace(/(\?s=)\d*$/i, '$196') - } else { - photo = generateAvatarURL(profile.username) - } - break - case 'mattermost': - photo = profile.avatarUrl - if (photo) { - if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') - else photo = photo.replace(/(\?s=)\d*$/i, '$196') - } else { - photo = generateAvatarURL(profile.username) - } - break - case 'dropbox': - photo = generateAvatarURL('', profile.emails[0].value, bigger) - break - case 'google': - photo = profile.photos[0].value - if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400') - else photo = photo.replace(/(\?sz=)\d*$/i, '$196') - break - case 'ldap': - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - case 'saml': - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - } - return photo - }, - parseProfileByEmail: function (email) { - return { - name: email.substring(0, email.lastIndexOf('@')), - photo: generateAvatarURL('', email, false), - biggerphoto: generateAvatarURL('', email, true) - } + }) + + User.prototype.verifyPassword = function (attempt) { + if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) { + return this + } else { + return false + } + } + + User.associate = function (models) { + User.hasMany(models.Note, { + foreignKey: 'ownerId', + constraints: false + }) + User.hasMany(models.Note, { + foreignKey: 'lastchangeuserId', + constraints: false + }) + } + User.getProfile = function (user) { + if (!user) { + return null + } + return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) + } + User.parseProfile = function (profile) { + try { + profile = JSON.parse(profile) + } catch (err) { + logger.error(err) + profile = null + } + if (profile) { + profile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile), + biggerphoto: User.parsePhotoByProfile(profile, true) } } - }) + return profile + } + User.parsePhotoByProfile = function (profile, bigger) { + var photo = null + switch (profile.provider) { + case 'facebook': + photo = 'https://graph.facebook.com/' + profile.id + '/picture' + if (bigger) photo += '?width=400' + else photo += '?width=96' + break + case 'twitter': + photo = 'https://twitter.com/' + profile.username + '/profile_image' + if (bigger) photo += '?size=original' + else photo += '?size=bigger' + break + case 'github': + photo = 'https://avatars.githubusercontent.com/u/' + profile.id + if (bigger) photo += '?s=400' + else photo += '?s=96' + break + case 'gitlab': + photo = profile.avatarUrl + if (photo) { + if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') + else photo = photo.replace(/(\?s=)\d*$/i, '$196') + } else { + photo = generateAvatarURL(profile.username) + } + break + case 'mattermost': + photo = profile.avatarUrl + if (photo) { + if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') + else photo = photo.replace(/(\?s=)\d*$/i, '$196') + } else { + photo = generateAvatarURL(profile.username) + } + break + case 'dropbox': + photo = generateAvatarURL('', profile.emails[0].value, bigger) + break + case 'google': + photo = profile.photos[0].value + if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400') + else photo = photo.replace(/(\?sz=)\d*$/i, '$196') + break + case 'ldap': + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) + break + case 'saml': + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) + break + } + return photo + } + User.parseProfileByEmail = function (email) { + return { + name: email.substring(0, email.lastIndexOf('@')), + photo: generateAvatarURL('', email, false), + biggerphoto: generateAvatarURL('', email, true) + } + } return User } diff --git a/lib/response.js b/lib/response.js index 76793a46..b943da22 100644 --- a/lib/response.js +++ b/lib/response.js @@ -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 { @@ -549,16 +549,16 @@ function gitlabActionProjects (req, res, note) { ret.accesstoken = user.accessToken ret.profileid = user.profileid request( - config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken, - function (error, httpResponse, body) { - if (!error && httpResponse.statusCode === 200) { - ret.projects = JSON.parse(body) - return res.send(ret) - } else { - return res.send(ret) - } - } - ) + config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken, + function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 200) { + ret.projects = JSON.parse(body) + return res.send(ret) + } else { + return res.send(ret) + } + } + ) }).catch(function (err) { logger.error('gitlab action projects failed: ' + err) return response.errorInternalError(res) diff --git a/lib/web/auth/dropbox/index.js b/lib/web/auth/dropbox/index.js index c44607da..1cfabd29 100644 --- a/lib/web/auth/dropbox/index.js +++ b/lib/web/auth/dropbox/index.js @@ -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() diff --git a/lib/web/auth/email/index.js b/lib/web/auth/email/index.js index f7e58d46..7c812d69 100644 --- a/lib/web/auth/email/index.js +++ b/lib/web/auth/email/index.js @@ -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() diff --git a/lib/web/auth/facebook/index.js b/lib/web/auth/facebook/index.js index b2364989..418ddeee 100644 --- a/lib/web/auth/facebook/index.js +++ b/lib/web/auth/facebook/index.js @@ -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() diff --git a/lib/web/auth/github/index.js b/lib/web/auth/github/index.js index ece634ba..afa5fa31 100644 --- a/lib/web/auth/github/index.js +++ b/lib/web/auth/github/index.js @@ -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() diff --git a/lib/web/auth/gitlab/index.js b/lib/web/auth/gitlab/index.js index 38436024..4cebbc10 100644 --- a/lib/web/auth/gitlab/index.js +++ b/lib/web/auth/gitlab/index.js @@ -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() diff --git a/lib/web/auth/google/index.js b/lib/web/auth/google/index.js index 0a4fd55e..ad9bcd7a 100644 --- a/lib/web/auth/google/index.js +++ b/lib/web/auth/google/index.js @@ -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 + '/', diff --git a/lib/web/auth/ldap/index.js b/lib/web/auth/ldap/index.js index 8d71c18e..77790db3 100644 --- a/lib/web/auth/ldap/index.js +++ b/lib/web/auth/ldap/index.js @@ -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() diff --git a/lib/web/auth/mattermost/index.js b/lib/web/auth/mattermost/index.js index 63a4886f..3a5f7853 100644 --- a/lib/web/auth/mattermost/index.js +++ b/lib/web/auth/mattermost/index.js @@ -1,16 +1,17 @@ 'use strict' - +require('babel-polyfill') +require('isomorphic-fetch'); const Router = require('express').Router const passport = require('passport') -const Mattermost = require('mattermost') +const MattermostClient = require('mattermost-redux/client/client4').default const OAuthStrategy = require('passport-oauth2').Strategy const config = require('../../../config') -const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') - -const mattermost = new Mattermost.Client() +const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') let mattermostAuth = module.exports = Router() +const mattermostClient = new MattermostClient() + let mattermostStrategy = new OAuthStrategy({ authorizationURL: config.mattermost.baseURL + '/oauth/authorize', tokenURL: config.mattermost.baseURL + '/oauth/access_token', @@ -20,17 +21,11 @@ let mattermostStrategy = new OAuthStrategy({ }, passportGeneralCallback) mattermostStrategy.userProfile = (accessToken, done) => { - mattermost.setUrl(config.mattermost.baseURL) - mattermost.token = accessToken - mattermost.useHeaderToken() - mattermost.getMe( - (data) => { - done(null, data) - }, - (err) => { - done(err) - } - ) + mattermostClient.setUrl(config.mattermost.baseURL) + mattermostClient.setToken(accessToken) + mattermostClient.getMe() + .then((data) => done(null, data)) + .catch((err) => done(err)) } passport.use(mattermostStrategy) diff --git a/lib/web/auth/oauth2/index.js b/lib/web/auth/oauth2/index.js index 57ab9b9a..78434271 100644 --- a/lib/web/auth/oauth2/index.js +++ b/lib/web/auth/oauth2/index.js @@ -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() diff --git a/lib/web/auth/openid/index.js b/lib/web/auth/openid/index.js index c45c6d71..8d271a7a 100644 --- a/lib/web/auth/openid/index.js +++ b/lib/web/auth/openid/index.js @@ -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() diff --git a/lib/web/auth/saml/index.js b/lib/web/auth/saml/index.js index b8d98340..9d65339f 100644 --- a/lib/web/auth/saml/index.js +++ b/lib/web/auth/saml/index.js @@ -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)) } diff --git a/lib/web/auth/twitter/index.js b/lib/web/auth/twitter/index.js index c1860d93..5aba20ff 100644 --- a/lib/web/auth/twitter/index.js +++ b/lib/web/auth/twitter/index.js @@ -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() diff --git a/lib/web/historyRouter.js b/lib/web/historyRouter.js index 1b22c232..fa426bbb 100644 --- a/lib/web/historyRouter.js +++ b/lib/web/historyRouter.js @@ -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() diff --git a/lib/web/imageRouter/imgur.js b/lib/web/imageRouter/imgur.js index 2a20002c..11dac6ad 100644 --- a/lib/web/imageRouter/imgur.js +++ b/lib/web/imageRouter/imgur.js @@ -2,7 +2,7 @@ const config = require('../../config') const logger = require('../../logger') -const imgur = require('imgur') +const imgur = require('@hackmd/imgur') exports.uploadImage = function (imagePath, callback) { if (!imagePath || typeof imagePath !== 'string') { @@ -17,12 +17,12 @@ exports.uploadImage = function (imagePath, callback) { imgur.setClientId(config.imgur.clientID) imgur.uploadFile(imagePath) - .then(function (json) { - if (config.debug) { - logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) - } - callback(null, json.data.link.replace(/^http:\/\//i, 'https://')) - }).catch(function (err) { - callback(new Error(err), null) - }) + .then(function (json) { + if (config.debug) { + logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) + } + callback(null, json.data.link.replace(/^http:\/\//i, 'https://')) + }).catch(function (err) { + callback(new Error(err), null) + }) } diff --git a/lib/web/imageRouter/minio.js b/lib/web/imageRouter/minio.js index b921c2d2..f72b31c3 100644 --- a/lib/web/imageRouter/minio.js +++ b/lib/web/imageRouter/minio.js @@ -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') diff --git a/lib/web/imageRouter/s3.js b/lib/web/imageRouter/s3.js index 626fe148..b0eca7b5 100644 --- a/lib/web/imageRouter/s3.js +++ b/lib/web/imageRouter/s3.js @@ -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') diff --git a/lib/web/noteRouter.js b/lib/web/noteRouter.js index 41bf5f73..bac2cf88 100644 --- a/lib/web/noteRouter.js +++ b/lib/web/noteRouter.js @@ -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() diff --git a/lib/web/statusRouter.js b/lib/web/statusRouter.js index 2b9cb65f..6f797f84 100644 --- a/lib/web/statusRouter.js +++ b/lib/web/statusRouter.js @@ -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() diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index ca364422..73b519ec 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -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() diff --git a/lib/workers/dmpWorker.js b/lib/workers/dmpWorker.js index 60db0a12..c00952d1 100644 --- a/lib/workers/dmpWorker.js +++ b/lib/workers/dmpWorker.js @@ -1,6 +1,6 @@ 'use strict' // external modules -var DiffMatchPatch = require('diff-match-patch') +var DiffMatchPatch = require('@hackmd/diff-match-patch') var dmp = new DiffMatchPatch() // core diff --git a/package.json b/package.json index a3da0ba7..9441dae0 100644 --- a/package.json +++ b/package.json @@ -1,152 +1,194 @@ { - "name": "CodiMD", + "name": "codimd", "version": "1.3.1", "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", - "jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done", - "standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1", - "dev": "webpack --config webpack.dev.js --progress --colors --watch", - "build": "webpack --config webpack.prod.js --progress --colors --bail", - "postinstall": "bin/heroku", - "start": "sequelize db:migrate && node app.js", - "doctoc": "doctoc --title='# Table of Contents' README.md" - }, - "dependencies": { - "@passport-next/passport-openid": "^1.0.0", - "Idle.Js": "git+https://github.com/shawnmclean/Idle.js", - "archiver": "^2.1.1", - "async": "^2.1.4", - "aws-sdk": "^2.345.0", - "azure-storage": "^2.7.0", - "base64url": "^3.0.0", - "body-parser": "^1.15.2", - "bootstrap": "^3.4.0", - "bootstrap-validator": "^0.11.8", - "chance": "^1.0.4", - "cheerio": "^0.22.0", - "codemirror": "git+https://github.com/hackmdio/CodeMirror.git", - "compression": "^1.6.2", - "connect-flash": "^0.1.1", - "connect-session-sequelize": "^4.1.0", - "cookie": "0.3.1", - "cookie-parser": "1.4.3", - "deep-freeze": "^0.0.1", - "diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git", - "ejs": "^2.5.5", - "emojify.js": "~1.1.0", - "express": ">=4.14", - "express-session": "^1.14.2", - "file-saver": "^1.3.3", - "flowchart.js": "^1.6.4", - "fork-awesome": "^1.1.3", - "formidable": "^1.0.17", - "gist-embed": "~2.6.0", - "graceful-fs": "^4.1.11", - "handlebars": "^4.0.13", - "helmet": "^3.13.0", - "highlight.js": "~9.12.0", - "i18n": "^0.8.3", - "imgur": "git+https://github.com/hackmdio/node-imgur.git", - "ionicons": "~2.0.1", - "jquery": "^3.1.1", - "jquery-mousewheel": "^3.1.13", - "jquery-ui": "^1.12.1", - "js-cookie": "^2.1.3", - "js-sequence-diagrams": "^1000000.0.6", - "js-url": "^2.3.0", - "js-yaml": "^3.7.0", - "jsdom-nogyp": "^0.8.3", - "keymaster": "^1.6.2", - "list.js": "^1.5.0", - "lodash": "^4.17.11", - "lz-string": "git+https://github.com/hackmdio/lz-string.git", - "markdown-it": "^8.2.2", - "markdown-it-abbr": "^1.0.4", - "markdown-it-container": "^2.0.0", - "markdown-it-deflist": "^2.0.1", - "markdown-it-emoji": "^1.3.0", - "markdown-it-footnote": "^3.0.1", - "markdown-it-imsize": "^2.0.1", - "markdown-it-ins": "^2.0.0", - "markdown-it-mark": "^2.0.0", - "markdown-it-mathjax": "^2.0.0", - "markdown-it-regexp": "^0.4.0", - "markdown-it-sub": "^1.0.0", - "markdown-it-sup": "^1.0.0", - "markdown-pdf": "^9.0.0", - "mathjax": "~2.7.0", - "mattermost": "^3.4.0", - "mermaid": "~7.1.0", - "meta-marked": "^0.4.2", - "method-override": "^2.3.7", - "minimist": "^1.2.0", - "minio": "^6.0.0", - "moment": "^2.17.1", - "morgan": "^1.7.0", - "mysql": "^2.12.0", - "passport": "^0.4.0", - "passport-dropbox-oauth2": "^1.1.0", - "passport-facebook": "^2.1.1", - "passport-github": "^1.1.0", - "passport-gitlab2": "^4.0.0", - "passport-google-oauth20": "^1.0.0", - "passport-ldapauth": "^2.0.0", - "passport-local": "^1.0.0", - "passport-oauth2": "^1.4.0", - "passport-saml": "^1.0.0", - "passport-twitter": "^1.0.4", - "passport.socketio": "^3.7.0", - "pdfobject": "^2.0.201604172", - "pg": "^6.1.2", - "pg-hstore": "^2.3.2", - "prismjs": "^1.6.0", - "randomcolor": "^0.5.3", - "raphael": "git+https://github.com/dmitrybaranovskiy/raphael", - "readline-sync": "^1.4.7", - "request": "^2.88.0", - "reveal.js": "~3.7.0", - "@mlink/scrypt": "^6.1.2", - "select2": "^3.5.2-browserify", - "sequelize": "^3.28.0", - "sequelize-cli": "^2.5.1", - "shortid": "2.2.8", - "socket.io": "~2.1.1", - "socket.io-client": "~2.1.1", - "spin.js": "^2.3.2", - "sqlite3": "^4.0.1", - "store": "^2.0.12", - "string": "^3.3.3", - "tedious": "^1.14.0", - "toobusy-js": "^0.5.1", - "turndown": "^5.0.1", - "uuid": "^3.1.0", - "validator": "^10.4.0", - "velocity-animate": "^1.4.0", - "visibilityjs": "^1.2.4", - "viz.js": "^1.7.0", - "winston": "^3.1.0", - "ws": "^6.0.0", - "xss": "^1.0.3" - }, - "resolutions": { - "**/tough-cookie": "~2.4.0", - "**/minimatch": "^3.0.2", - "**/request": "^2.88.0" - }, - "engines": { - "node": ">=6.x" - }, - "bugs": "https://github.com/hackmdio/codimd/issues", "keywords": [ "Collaborative", "Markdown", "Notes" ], - "homepage": "https://codimd.org", + "bugs": "https://github.com/hackmdio/codimd/issues", + "repository": { + "type": "git", + "url": "https://github.com/hackmdio/codimd.git" + }, + "license": "AGPL-3.0", + "main": "app.js", + "scripts": { + "build": "webpack --config webpack.prod.js --progress --colors --bail", + "dev": "webpack --config webpack.dev.js --progress --colors --watch", + "doctoc": "doctoc --title='# Table of Contents' README.md", + "eslint": "eslint lib public test app.js", + "postinstall": "bin/heroku", + "jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done", + "standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1", + "start": "sequelize db:migrate && node app.js", + "test": "npm run-script eslint && npm run-script jsonlint && mocha" + }, + "dependencies": { + "@hackmd/codemirror": "~5.41.2", + "@hackmd/diff-match-patch": "~1.1.1", + "@hackmd/idle-js": "~1.0.1", + "@hackmd/imgur": "~0.4.1", + "@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3", + "@hackmd/lz-string": "~1.4.4", + "@hackmd/meta-marked": "~0.4.4", + "@passport-next/passport-openid": "~1.0.0", + "archiver": "~2.1.1", + "async": "~2.1.4", + "aws-sdk": "~2.345.0", + "azure-storage": "~2.10.2", + "base64url": "~3.0.0", + "body-parser": "~1.18.3", + "bootstrap": "~3.4.0", + "bootstrap-validator": "~0.11.8", + "chance": "~1.0.4", + "cheerio": "~0.22.0", + "compression": "~1.7.4", + "connect-flash": "~0.1.1", + "connect-session-sequelize": "~6.0.0", + "cookie": "~0.3.1", + "cookie-parser": "~1.4.3", + "deep-freeze": "~0.0.1", + "ejs": "~2.5.5", + "emojify.js": "~1.1.0", + "express": "~4.16.4", + "express-session": "~1.16.1", + "file-saver": "~1.3.3", + "flowchart.js": "~1.6.4", + "fork-awesome": "~1.1.3", + "formidable": "~1.2.1", + "gist-embed": "~2.6.0", + "graceful-fs": "~4.1.11", + "handlebars": "~4.0.13", + "helmet": "~3.13.0", + "highlight.js": "~9.12.0", + "i18n": "~0.8.3", + "ionicons": "~2.0.1", + "isomorphic-fetch": "^2.2.1", + "jquery": "~3.1.1", + "jquery-mousewheel": "~3.1.13", + "jquery-ui": "~1.12.1", + "js-cookie": "~2.1.3", + "js-yaml": "~3.13.1", + "jsdom-nogyp": "~0.8.3", + "keymaster": "~1.6.2", + "list.js": "~1.5.0", + "lodash": "~4.17.11", + "markdown-it": "~8.2.2", + "markdown-it-abbr": "~1.0.4", + "markdown-it-container": "~2.0.0", + "markdown-it-deflist": "~2.0.1", + "markdown-it-emoji": "~1.3.0", + "markdown-it-footnote": "~3.0.1", + "markdown-it-imsize": "~2.0.1", + "markdown-it-ins": "~2.0.0", + "markdown-it-mark": "~2.0.0", + "markdown-it-mathjax": "~2.0.0", + "markdown-it-regexp": "~0.4.0", + "markdown-it-sub": "~1.0.0", + "markdown-it-sup": "~1.0.0", + "markdown-pdf": "~9.0.0", + "mathjax": "~2.7.0", + "mattermost-redux": "^5.9.0", + "mermaid": "~7.1.0", + "method-override": "~2.3.7", + "minimist": "~1.2.0", + "minio": "~6.0.0", + "moment": "~2.24.0", + "morgan": "~1.9.1", + "mysql": "~2.16.0", + "passport": "~0.4.0", + "passport-dropbox-oauth2": "~1.1.0", + "passport-facebook": "~2.1.1", + "passport-github": "~1.1.0", + "passport-gitlab2": "~4.0.0", + "passport-google-oauth20": "~1.0.0", + "passport-ldapauth": "~2.1.3", + "passport-local": "~1.0.0", + "passport-oauth2": "~1.4.0", + "passport-saml": "~1.0.0", + "passport-twitter": "~1.0.4", + "passport.socketio": "~3.7.0", + "pdfobject": "~2.0.201604172", + "pg": "~6.1.2", + "pg-hstore": "~2.3.2", + "prismjs": "~1.6.0", + "randomcolor": "~0.5.3", + "raphael": "~2.2.8", + "readline-sync": "~1.4.7", + "request": "~2.88.0", + "reveal.js": "~3.7.0", + "scrypt": "~6.0.3", + "select2": "~3.5.2-browserify", + "sequelize": "5.3.5", + "shortid": "~2.2.8", + "socket.io": "~2.1.1", + "socket.io-client": "~2.1.1", + "spin.js": "~2.3.2", + "sqlite3": "~4.0.1", + "store": "~2.0.12", + "tedious": "~6.1.0", + "toobusy-js": "~0.5.1", + "turndown": "~5.0.1", + "uuid": "~3.1.0", + "validator": "~10.4.0", + "velocity-animate": "~1.4.0", + "visibilityjs": "~1.2.4", + "viz.js": "~1.7.0", + "winston": "~3.1.0", + "ws": "~6.0.0", + "wurl": "~2.5.3", + "xss": "~1.0.3" + }, + "devDependencies": { + "acorn": "~6.1.1", + "babel-core": "~6.26.3", + "babel-loader": "~7.1.4", + "babel-plugin-transform-runtime": "~6.23.0", + "babel-polyfill": "~6.26.0", + "babel-preset-env": "~1.7.0", + "babel-runtime": "~6.26.0", + "copy-webpack-plugin": "~4.5.2", + "css-loader": "~1.0.0", + "doctoc": "~1.4.0", + "ejs-loader": "~0.3.1", + "eslint": "~5.16.0", + "eslint-config-standard": "~12.0.0", + "eslint-plugin-import": "~2.17.1", + "eslint-plugin-node": "~8.0.1", + "eslint-plugin-promise": "~4.1.1", + "eslint-plugin-standard": "~4.0.0", + "exports-loader": "~0.7.0", + "expose-loader": "~0.7.5", + "file-loader": "~2.0.0", + "html-webpack-plugin": "~4.0.0-beta.2", + "imports-loader": "~0.8.0", + "jsonlint": "~1.6.2", + "less": "~3.9.0", + "less-loader": "~4.1.0", + "mini-css-extract-plugin": "~0.4.1", + "mocha": "~5.2.0", + "mock-require": "~3.0.3", + "optimize-css-assets-webpack-plugin": "~5.0.0", + "script-loader": "~0.7.2", + "sequelize-cli": "~5.4.0", + "string-loader": "~0.0.1", + "style-loader": "~0.21.0", + "uglifyjs-webpack-plugin": "~1.2.7", + "url-loader": "~1.0.1", + "webpack": "~4.30.0", + "webpack-cli": "~3.3.0", + "webpack-merge": "~4.1.4", + "webpack-parallel-uglify-plugin": "~1.1.0" + }, + "optionalDependencies": { + "bufferutil": "~4.0.0", + "utf-8-validate": "~5.0.1" + }, + "engines": { + "node": ">=8.0.0" + }, "maintainers": [ { "name": "Max Wu", @@ -156,53 +198,5 @@ "name": "Christoph (Sheogorath) Kern", "email": "codimd@sheogorath.shivering-isles.com" } - ], - "repository": { - "type": "git", - "url": "https://github.com/hackmdio/codimd.git" - }, - "devDependencies": { - "babel-cli": "^6.26.0", - "babel-core": "^6.26.3", - "babel-loader": "^7.1.4", - "babel-plugin-transform-runtime": "^6.23.0", - "babel-polyfill": "^6.26.0", - "babel-preset-env": "^1.7.0", - "babel-runtime": "^6.26.0", - "copy-webpack-plugin": "^4.5.2", - "css-loader": "^1.0.0", - "doctoc": "^1.4.0", - "ejs-loader": "^0.3.1", - "eslint": "^5.9.0", - "eslint-config-standard": "^12.0.0", - "eslint-plugin-import": "^2.14.0", - "eslint-plugin-node": "^8.0.0", - "eslint-plugin-promise": "^4.0.1", - "eslint-plugin-standard": "^4.0.0", - "exports-loader": "^0.7.0", - "expose-loader": "^0.7.5", - "file-loader": "^2.0.0", - "html-webpack-plugin": "4.0.0-beta.2", - "imports-loader": "^0.8.0", - "jsonlint": "^1.6.2", - "less": "^2.7.1", - "less-loader": "^4.1.0", - "mini-css-extract-plugin": "^0.4.1", - "mocha": "^5.2.0", - "mock-require": "^3.0.3", - "optimize-css-assets-webpack-plugin": "^5.0.0", - "script-loader": "^0.7.2", - "string-loader": "^0.0.1", - "style-loader": "^0.21.0", - "uglifyjs-webpack-plugin": "^1.2.7", - "url-loader": "^1.0.1", - "webpack": "^4.14.0", - "webpack-cli": "^3.1.0", - "webpack-merge": "^4.1.4", - "webpack-parallel-uglify-plugin": "^1.1.0" - }, - "optionalDependencies": { - "bufferutil": "^4.0.0", - "utf-8-validate": "^5.0.1" - } + ] } diff --git a/public/.eslintrc.js b/public/.eslintrc.js index dc37b3cb..0ca21e4e 100644 --- a/public/.eslintrc.js +++ b/public/.eslintrc.js @@ -12,7 +12,6 @@ module.exports = { "ui": false, "Spinner": false, "modeType": false, - "Idle": false, "serverurl": false, "key": false, "gapi": false, diff --git a/public/css/cover.css b/public/css/cover.css index 6e191d2b..4693c9c3 100644 --- a/public/css/cover.css +++ b/public/css/cover.css @@ -371,4 +371,42 @@ select { .ui-use-tags { max-width: 100%; } -} \ No newline at end of file +} + +.btn-mattermost { + background-color: #2179ec; + border-color: rgba(0,0,0,0.2); + color: #fff; +} + +.btn-gitlab { + background-color: #e35431; + border-color: rgba(0,0,0,0.2); + color: #fff; +} + +.btn-mattermost:hover, .btn-mattermost:active { + background-color: #105fc6; + border-color: rgba(0,0,0,0.2); + color: #fff; +} + +.btn-gitlab:hover, .btn-gitlab:active { + background-color: #c23b1a; + border-color: rgba(0,0,0,0.2); + color: #fff; +} + +a.btn.btn-social > i.oauth-icon { + display: inline-flex; + height: 45px; + width: 45px; + line-height: inherit; + padding: 6px; +} + +a.btn.btn-social > i.oauth-icon > img { + width: 100%; + height: 100%; + line-height: inherit; +} diff --git a/public/images/mattermost-logo.svg b/public/images/mattermost-logo.svg new file mode 100644 index 00000000..c39d70cf --- /dev/null +++ b/public/images/mattermost-logo.svg @@ -0,0 +1,33 @@ + + + + + diff --git a/public/js/cover.js b/public/js/cover.js index 79fb3a2a..10111bfa 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -1,37 +1,37 @@ /* eslint-env browser, jquery */ /* global moment, serverurl */ +import { + checkIfAuth, + clearLoginState, + getLoginState, + resetCheckAuth, + setloginStateChangeEvent +} from './lib/common/login' + +import { + clearDuplicatedHistory, + deleteServerHistory, + getHistory, + getStorageHistory, + parseHistory, + parseServerToHistory, + parseStorageToHistory, + postHistoryToServer, + removeHistory, + saveHistory, + saveStorageHistoryToServer +} from './history' + +import { saveAs } from 'file-saver' +import List from 'list.js' +import unescapeHTML from 'lodash/unescape' + require('./locale') require('../css/cover.css') require('../css/site.css') -import { - checkIfAuth, - clearLoginState, - getLoginState, - resetCheckAuth, - setloginStateChangeEvent -} from './lib/common/login' - -import { - clearDuplicatedHistory, - deleteServerHistory, - getHistory, - getStorageHistory, - parseHistory, - parseServerToHistory, - parseStorageToHistory, - postHistoryToServer, - removeHistory, - saveHistory, - saveStorageHistoryToServer -} from './history' - -import { saveAs } from 'file-saver' -import List from 'list.js' -import S from 'string' - const options = { valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'], item: `
  • @@ -67,27 +67,27 @@ pageInit() function pageInit () { checkIfAuth( - data => { - $('.ui-signin').hide() - $('.ui-or').hide() - $('.ui-welcome').show() - if (data.photo) $('.ui-avatar').prop('src', data.photo).show() - else $('.ui-avatar').prop('src', '').hide() - $('.ui-name').html(data.name) - $('.ui-signout').show() - $('.ui-history').click() - parseServerToHistory(historyList, parseHistoryCallback) - }, - () => { - $('.ui-signin').show() - $('.ui-or').show() - $('.ui-welcome').hide() - $('.ui-avatar').prop('src', '').hide() - $('.ui-name').html('') - $('.ui-signout').hide() - parseStorageToHistory(historyList, parseHistoryCallback) - } - ) + data => { + $('.ui-signin').hide() + $('.ui-or').hide() + $('.ui-welcome').show() + if (data.photo) $('.ui-avatar').prop('src', data.photo).show() + else $('.ui-avatar').prop('src', '').hide() + $('.ui-name').html(data.name) + $('.ui-signout').show() + $('.ui-history').click() + parseServerToHistory(historyList, parseHistoryCallback) + }, + () => { + $('.ui-signin').show() + $('.ui-or').show() + $('.ui-welcome').hide() + $('.ui-avatar').prop('src', '').hide() + $('.ui-name').html('') + $('.ui-signout').hide() + parseStorageToHistory(historyList, parseHistoryCallback) + } + ) } $('.masthead-nav li').click(function () { @@ -132,7 +132,7 @@ function checkHistoryList () { function parseHistoryCallback (list, notehistory) { checkHistoryList() - // sort by pinned then timestamp + // sort by pinned then timestamp list.sort('', { sortFunction (a, b) { const notea = a.values() @@ -152,13 +152,13 @@ function parseHistoryCallback (list, notehistory) { } } }) - // parse filter tags + // parse filter tags const filtertags = [] for (let i = 0, l = list.items.length; i < l; i++) { const tags = list.items[i]._values.tags if (tags && tags.length > 0) { for (let j = 0; j < tags.length; j++) { - // push info filtertags if not found + // push info filtertags if not found let found = false if (filtertags.includes(tags[j])) { found = true } if (!found) { filtertags.push(tags[j]) } @@ -178,20 +178,20 @@ historyList.on('updated', e => { const a = itemEl.find('a') const pin = itemEl.find('.ui-history-pin') const tagsEl = itemEl.find('.tags') - // parse link to element a + // parse link to element a a.attr('href', `${serverurl}/${values.id}`) - // parse pinned + // parse pinned if (values.pinned) { pin.addClass('active') } else { pin.removeClass('active') } - // parse tags + // parse tags const tags = values.tags if (tags && tags.length > 0 && tagsEl.children().length <= 0) { const labels = [] for (let j = 0; j < tags.length; j++) { - // push into the item label + // push into the item label labels.push(`${tags[j]}`) } tagsEl.html(labels.join(' ')) @@ -328,7 +328,7 @@ $('.ui-open-history').bind('change', e => { const reader = new FileReader() reader.onload = () => { const notehistory = JSON.parse(reader.result) - // console.log(notehistory); + // console.log(notehistory); if (!reader.result) return getHistory(data => { let mergedata = data.concat(notehistory) @@ -397,7 +397,7 @@ function buildTagsFilter (tags) { for (let i = 0; i < tags.length; i++) { tags[i] = { id: i, - text: S(tags[i]).unescapeHTML().s + text: unescapeHTML(tags[i]) } } filtertags = tags diff --git a/public/js/extra.js b/public/js/extra.js index b80290d1..fb78a8c4 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -1,6 +1,24 @@ /* eslint-env browser, jquery */ /* global moment, serverurl */ +import Prism from 'prismjs' +import hljs from 'highlight.js' +import PDFObject from 'pdfobject' +import { saveAs } from 'file-saver' + +import escapeHTML from 'lodash/escape' +import unescapeHTML from 'lodash/unescape' + +import { stripTags } from '../../utils/string' + +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,17 +28,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' - 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 @@ -157,7 +167,7 @@ export function renderTags (view) { function slugifyWithUTF8 (text) { // remove html tags and trim spaces - let newText = S(text).trim().stripTags().s + let newText = stripTags(text.toString().trim()) // replace all spaces in between to dashes newText = newText.replace(/\s+/g, '-') // slugify string to make it valid for attribute @@ -190,7 +200,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) { dir = meta.dir breaks = meta.breaks } - // text language + // text language if (lang && typeof lang === 'string') { view.attr('lang', lang) toc.attr('lang', lang) @@ -202,7 +212,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) { tocAffix.removeAttr('lang') if (edit) { edit.removeAttr('lang', lang) } } - // text direction + // text direction if (dir && typeof dir === 'string') { view.attr('dir', dir) toc.attr('dir', dir) @@ -212,7 +222,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) { toc.removeAttr('dir') tocAffix.removeAttr('dir') } - // breaks + // breaks if (typeof breaks === 'boolean' && !breaks) { md.options.breaks = false } else { @@ -245,7 +255,7 @@ if (typeof window.mermaid !== 'undefined' && window.mermaid) window.mermaid.star // dynamic event or object binding here export function finishView (view) { - // todo list + // todo list const lis = view.find('li.raw').removeClass('raw').sortByDepth().toArray() for (let li of lis) { @@ -259,9 +269,9 @@ export function finishView (view) { li.innerHTML = html let disabled = 'disabled' if (typeof editor !== 'undefined' && window.havePermission()) { disabled = '' } - if (/^\s*\[[x ]\]\s*/.test(html)) { - li.innerHTML = html.replace(/^\s*\[ \]\s*/, ``) - .replace(/^\s*\[x\]\s*/, ``) + if (/^\s*\[[x ]]\s*/.test(html)) { + li.innerHTML = html.replace(/^\s*\[ ]\s*/, ``) + .replace(/^\s*\[x]\s*/, ``) if (li.tagName.toLowerCase() !== 'li') { li.parentElement.setAttribute('class', 'task-list-item') } else { @@ -269,42 +279,42 @@ export function finishView (view) { } } if (typeof editor !== 'undefined' && window.havePermission()) { $(li).find('input').change(toggleTodoEvent) } - // color tag in list will convert it to tag icon with color + // color tag in list will convert it to tag icon with color const tagColor = $(li).closest('ul').find('.color') tagColor.each((key, value) => { $(value).addClass('fa fa-tag').css('color', $(value).attr('data-color')) }) } - // youtube + // youtube view.find('div.youtube.raw').removeClass('raw') - .click(function () { - imgPlayiframe(this, '//www.youtube.com/embed/') - }) + .click(function () { + imgPlayiframe(this, '//www.youtube.com/embed/') + }) // vimeo view.find('div.vimeo.raw').removeClass('raw') - .click(function () { - imgPlayiframe(this, '//player.vimeo.com/video/') - }) - .each((key, value) => { - $.ajax({ - type: 'GET', - url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`, - jsonp: 'callback', - dataType: 'jsonp', - success (data) { - const thumbnailSrc = data[0].thumbnail_large - const image = `` - $(value).prepend(image) - if (window.viewAjaxCallback) window.viewAjaxCallback() - } - }) - }) + .click(function () { + imgPlayiframe(this, '//player.vimeo.com/video/') + }) + .each((key, value) => { + $.ajax({ + type: 'GET', + url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`, + jsonp: 'callback', + dataType: 'jsonp', + success (data) { + const thumbnailSrc = data[0].thumbnail_large + const image = `` + $(value).prepend(image) + if (window.viewAjaxCallback) window.viewAjaxCallback() + } + }) + }) // gist view.find('code[data-gist-id]').each((key, value) => { if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) } }) - // sequence diagram + // sequence diagram const sequences = view.find('div.sequence-diagram.raw').removeClass('raw') sequences.each((key, value) => { try { @@ -323,11 +333,11 @@ export function finishView (view) { svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet') } catch (err) { $value.unwrap() - $value.parent().append('
    ' + err + '
    ') + $value.parent().append(`
    ${escapeHTML(err)}
    `) console.warn(err) } }) - // flowchart + // flowchart const flow = view.find('div.flow-chart.raw').removeClass('raw') flow.each((key, value) => { try { @@ -347,11 +357,11 @@ export function finishView (view) { $value.children().unwrap().unwrap() } catch (err) { $value.unwrap() - $value.parent().append('
    ' + err + '
    ') + $value.parent().append(`
    ${escapeHTML(err)}
    `) console.warn(err) } }) - // graphviz + // graphviz var graphvizs = view.find('div.graphviz.raw').removeClass('raw') graphvizs.each(function (key, value) { try { @@ -366,11 +376,11 @@ export function finishView (view) { $value.children().unwrap().unwrap() } catch (err) { $value.unwrap() - $value.parent().append('
    ' + err + '
    ') + $value.parent().append(`
    ${escapeHTML(err)}
    `) console.warn(err) } }) - // mermaid + // mermaid const mermaids = view.find('div.mermaid.raw').removeClass('raw') mermaids.each((key, value) => { try { @@ -388,7 +398,7 @@ export function finishView (view) { } $value.unwrap() - $value.parent().append('
    ' + errormessage + '
    ') + $value.parent().append(`
    ${escapeHTML(errormessage)}
    `) console.warn(errormessage) } }) @@ -408,20 +418,20 @@ export function finishView (view) { svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet') } catch (err) { $value.unwrap() - $value.parent().append('
    ' + err + '
    ') + $value.parent().append(`
    ${escapeHTML(err)}
    `) console.warn(err) } }) - // image href new window(emoji not included) + // image href new window(emoji not included) const images = view.find('img.raw[src]').removeClass('raw') images.each((key, value) => { - // if it's already wrapped by link, then ignore + // if it's already wrapped by link, then ignore const $value = $(value) $value[0].onload = e => { if (window.viewAjaxCallback) window.viewAjaxCallback() } }) - // blockquote + // blockquote const blockquote = view.find('blockquote.raw').removeClass('raw') const blockquoteP = blockquote.find('p') blockquoteP.each((key, value) => { @@ -429,96 +439,96 @@ export function finishView (view) { html = replaceExtraTags(html) $(value).html(html) }) - // color tag in blockquote will change its left border color + // color tag in blockquote will change its left border color const blockquoteColor = blockquote.find('.color') blockquoteColor.each((key, value) => { $(value).closest('blockquote').css('border-left-color', $(value).attr('data-color')) }) - // slideshare + // slideshare view.find('div.slideshare.raw').removeClass('raw') - .each((key, value) => { - $.ajax({ - type: 'GET', - url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, - jsonp: 'callback', - dataType: 'jsonp', - success (data) { - const $html = $(data.html) - const iframe = $html.closest('iframe') - const caption = $html.closest('div') - const inner = $('
    ').append(iframe) - const height = iframe.attr('height') - const width = iframe.attr('width') - const ratio = (height / width) * 100 - inner.css('padding-bottom', `${ratio}%`) - $(value).html(inner).append(caption) - if (window.viewAjaxCallback) window.viewAjaxCallback() - } - }) - }) + .each((key, value) => { + $.ajax({ + type: 'GET', + url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, + jsonp: 'callback', + dataType: 'jsonp', + success (data) { + const $html = $(data.html) + const iframe = $html.closest('iframe') + const caption = $html.closest('div') + const inner = $('
    ').append(iframe) + const height = iframe.attr('height') + const width = iframe.attr('width') + const ratio = (height / width) * 100 + inner.css('padding-bottom', `${ratio}%`) + $(value).html(inner).append(caption) + if (window.viewAjaxCallback) window.viewAjaxCallback() + } + }) + }) // speakerdeck view.find('div.speakerdeck.raw').removeClass('raw') - .each((key, value) => { - const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}` - const inner = $('Speakerdeck') - inner.attr('href', url) - inner.attr('rel', 'noopener noreferrer') - inner.attr('target', '_blank') - $(value).append(inner) - }) + .each((key, value) => { + const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}` + const inner = $('Speakerdeck') + inner.attr('href', url) + inner.attr('rel', 'noopener noreferrer') + inner.attr('target', '_blank') + $(value).append(inner) + }) // pdf view.find('div.pdf.raw').removeClass('raw') - .each(function (key, value) { - const url = $(value).attr('data-pdfurl') - const inner = $('
    ') - $(this).append(inner) - PDFObject.embed(url, inner, { - height: '400px' - }) - }) + .each(function (key, value) { + const url = $(value).attr('data-pdfurl') + const inner = $('
    ') + $(this).append(inner) + PDFObject.embed(url, inner, { + height: '400px' + }) + }) // syntax highlighting view.find('code.raw').removeClass('raw') - .each((key, value) => { - const langDiv = $(value) - if (langDiv.length > 0) { - const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim() - const codeDiv = langDiv.find('.code') - let code = '' - if (codeDiv.length > 0) code = codeDiv.html() - else code = langDiv.html() - var result - if (!reallang) { - result = { - value: code - } - } else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') { - code = S(code).unescapeHTML().s - result = { - value: Prism.highlight(code, Prism.languages[reallang]) - } - } else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') { - code = S(code).unescapeHTML().s - result = { - value: Prism.highlight(code, Prism.languages.wiki) - } - } else if (reallang === 'cmake') { - code = S(code).unescapeHTML().s - result = { - value: Prism.highlight(code, Prism.languages.makefile) - } - } else { - code = S(code).unescapeHTML().s - const languages = hljs.listLanguages() - if (!languages.includes(reallang)) { - result = hljs.highlightAuto(code) - } else { - result = hljs.highlight(reallang, code) - } - } - if (codeDiv.length > 0) codeDiv.html(result.value) - else langDiv.html(result.value) + .each((key, value) => { + const langDiv = $(value) + if (langDiv.length > 0) { + const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim() + const codeDiv = langDiv.find('.code') + let code = '' + if (codeDiv.length > 0) code = codeDiv.html() + else code = langDiv.html() + var result + if (!reallang) { + result = { + value: code } - }) + } else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') { + code = unescapeHTML(code) + result = { + value: Prism.highlight(code, Prism.languages[reallang]) + } + } else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') { + code = unescapeHTML(code) + result = { + value: Prism.highlight(code, Prism.languages.wiki) + } + } else if (reallang === 'cmake') { + code = unescapeHTML(code) + result = { + value: Prism.highlight(code, Prism.languages.makefile) + } + } else { + code = unescapeHTML(code) + const languages = hljs.listLanguages() + if (!languages.includes(reallang)) { + result = hljs.highlightAuto(code) + } else { + result = hljs.highlight(reallang, code) + } + } + if (codeDiv.length > 0) codeDiv.html(result.value) + else langDiv.html(result.value) + } + }) // mathjax const mathjaxdivs = view.find('span.mathjax.raw').removeClass('raw').toArray() try { @@ -532,7 +542,7 @@ export function finishView (view) { } catch (err) { console.warn(err) } - // render title + // render title document.title = renderTitle(view) } @@ -568,7 +578,7 @@ export function postProcess (code) { if (warning && warning.length > 0) { warning.text(md.metaError) } else { - warning = $('
    ' + md.metaError + '
    ') + warning = $(`
    ${escapeHTML(md.metaError)}
    `) result.prepend(warning) } } @@ -592,23 +602,23 @@ window.removeDOMEvents = removeDOMEvents function generateCleanHTML (view) { const src = view.clone() const eles = src.find('*') - // remove syncscroll parts + // remove syncscroll parts eles.removeClass('part') src.find('*[class=""]').removeAttr('class') eles.removeAttr('data-startline data-endline') src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll') - // remove gist content + // remove gist content src.find('code[data-gist-id]').children().remove() - // disable todo list + // disable todo list src.find('input.task-list-item-checkbox').attr('disabled', '') - // replace emoji image path + // replace emoji image path src.find('img.emoji').each((key, value) => { let name = $(value).attr('alt') name = name.substr(1) name = name.slice(0, name.length - 1) $(value).attr('src', `https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/images/basic/${name}.png`) }) - // replace video to iframe + // replace video to iframe src.find('div[data-videoid]').each((key, value) => { const id = $(value).attr('data-videoid') const style = $(value).attr('style') @@ -644,12 +654,12 @@ export function exportToHTML (view) { const title = renderTitle(ui.area.markdown) const filename = `${renderFilename(ui.area.markdown)}.html` const src = generateCleanHTML(view) - // generate toc + // generate toc const toc = $('#ui-toc').clone() toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll') const tocAffix = $('#ui-toc-affix').clone() tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll') - // generate html via template + // generate html via template $.get(`${serverurl}/build/html.min.css`, css => { $.get(`${serverurl}/views/html.hbs`, data => { const template = window.Handlebars.compile(data) @@ -664,7 +674,7 @@ export function exportToHTML (view) { dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null } const html = template(context) - // console.log(html); + // console.log(html); const blob = new Blob([html], { type: 'text/html;charset=utf-8' }) @@ -779,20 +789,20 @@ export function smoothHashScroll () { const hash = element.hash if (hash) { $element.on('click', function (e) { - // store hash + // store hash const hash = decodeURIComponent(this.hash) - // escape special characters in jquery selector + // escape special characters in jquery selector const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, '\\$1')) - // return if no element been selected + // return if no element been selected if ($hash.length <= 0) return - // prevent default anchor click behavior + // prevent default anchor click behavior e.preventDefault() - // animate + // animate $('body, html').stop(true, true).animate({ scrollTop: $hash.offset().top }, 100, 'linear', () => { - // when done, add hash to url - // (default click behaviour) + // when done, add hash to url + // (default click behaviour) window.location.hash = hash }) }) @@ -902,7 +912,7 @@ export function scrollToHash () { function highlightRender (code, lang) { if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return } - code = S(code).escapeHTML().s + code = escapeHTML(code) if (lang === 'sequence') { return `
    ${code}
    ` } else if (lang === 'flow') { @@ -934,9 +944,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, @@ -1034,109 +1041,106 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => { return `
    ${highlighted}
    \n` } -/* Defined regex markdown it plugins */ -import Plugin from 'markdown-it-regexp' - // youtube const youtubePlugin = new Plugin( - // regexp to match - /{%youtube\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%youtube\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const videoid = match[1] - if (!videoid) return - const div = $('
    ') - div.attr('data-videoid', videoid) - const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg` - const image = `` - div.append(image) - const icon = '' - div.append(icon) - return div[0].outerHTML - } + (match, utils) => { + const videoid = match[1] + if (!videoid) return + const div = $('
    ') + div.attr('data-videoid', videoid) + const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg` + const image = `` + div.append(image) + const icon = '' + div.append(icon) + return div[0].outerHTML + } ) // vimeo const vimeoPlugin = new Plugin( - // regexp to match - /{%vimeo\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%vimeo\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const videoid = match[1] - if (!videoid) return - const div = $('
    ') - div.attr('data-videoid', videoid) - const icon = '' - div.append(icon) - return div[0].outerHTML - } + (match, utils) => { + const videoid = match[1] + if (!videoid) return + const div = $('
    ') + div.attr('data-videoid', videoid) + const icon = '' + div.append(icon) + return div[0].outerHTML + } ) // gist const gistPlugin = new Plugin( - // regexp to match - /{%gist\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%gist\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const gistid = match[1] - const code = `` - return code - } + (match, utils) => { + const gistid = match[1] + const code = `` + return code + } ) // TOC const tocPlugin = new Plugin( - // regexp to match - /^\[TOC\]$/i, + // regexp to match + /^\[TOC\]$/i, - (match, utils) => '
    ' + (match, utils) => '
    ' ) // slideshare const slidesharePlugin = new Plugin( - // regexp to match - /{%slideshare\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%slideshare\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const slideshareid = match[1] - const div = $('
    ') - div.attr('data-slideshareid', slideshareid) - return div[0].outerHTML - } + (match, utils) => { + const slideshareid = match[1] + const div = $('
    ') + div.attr('data-slideshareid', slideshareid) + return div[0].outerHTML + } ) // speakerdeck const speakerdeckPlugin = new Plugin( - // regexp to match - /{%speakerdeck\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%speakerdeck\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const speakerdeckid = match[1] - const div = $('
    ') - div.attr('data-speakerdeckid', speakerdeckid) - return div[0].outerHTML - } + (match, utils) => { + const speakerdeckid = match[1] + const div = $('
    ') + div.attr('data-speakerdeckid', speakerdeckid) + return div[0].outerHTML + } ) // pdf const pdfPlugin = new Plugin( - // regexp to match - /{%pdf\s*([\d\D]*?)\s*%}/, + // regexp to match + /{%pdf\s*([\d\D]*?)\s*%}/, - (match, utils) => { - const pdfurl = match[1] - if (!isValidURL(pdfurl)) return match[0] - const div = $('
    ') - div.attr('data-pdfurl', pdfurl) - return div[0].outerHTML - } + (match, utils) => { + const pdfurl = match[1] + if (!isValidURL(pdfurl)) return match[0] + const div = $('
    ') + div.attr('data-pdfurl', pdfurl) + return div[0].outerHTML + } ) const emojijsPlugin = new Plugin( - // regexp to match emoji shortcodes :something: - // We generate an universal regex that guaranteed only contains the - // emojies we have available. This should prevent all false-positives - new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'), + // regexp to match emoji shortcodes :something: + // We generate an universal regex that guaranteed only contains the + // emojies we have available. This should prevent all false-positives + new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'), - (match, utils) => { - const emoji = match[1].toLowerCase() - const div = $(`:${emoji}:`) - return div[0].outerHTML - } + (match, utils) => { + const emoji = match[1].toLowerCase() + const div = $(`:${emoji}:`) + return div[0].outerHTML + } ) // yaml meta, from https://github.com/eugeneware/remarkable-meta diff --git a/public/js/history.js b/public/js/history.js index 6007bef4..6dc18548 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -2,65 +2,64 @@ /* global serverurl, moment */ import store from 'store' -import S from 'string' -import LZString from 'lz-string' +import LZString from '@hackmd/lz-string' + +import escapeHTML from 'lodash/escape' + +import wurl from 'wurl' import { checkNoteIdValid, encodeNoteId } from './utils' -import { - checkIfAuth -} from './lib/common/login' +import { checkIfAuth } from './lib/common/login' -import { - urlpath -} from './lib/config' +import { urlpath } from './lib/config' window.migrateHistoryFromTempCallback = null migrateHistoryFromTemp() function migrateHistoryFromTemp () { - if (window.url('#tempid')) { + if (wurl('#tempid')) { $.get(`${serverurl}/temp`, { - tempid: window.url('#tempid') + tempid: wurl('#tempid') }) - .done(data => { - if (data && data.temp) { - getStorageHistory(olddata => { - if (!olddata || olddata.length === 0) { - saveHistoryToStorage(JSON.parse(data.temp)) - } - }) - } - }) - .always(() => { - let hash = location.hash.split('#')[1] - hash = hash.split('&') - for (let i = 0; i < hash.length; i++) { - if (hash[i].indexOf('tempid') === 0) { - hash.splice(i, 1) - i-- + .done(data => { + if (data && data.temp) { + getStorageHistory(olddata => { + if (!olddata || olddata.length === 0) { + saveHistoryToStorage(JSON.parse(data.temp)) + } + }) } - } - hash = hash.join('&') - location.hash = hash - if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() } - }) + }) + .always(() => { + let hash = location.hash.split('#')[1] + hash = hash.split('&') + for (let i = 0; i < hash.length; i++) { + if (hash[i].indexOf('tempid') === 0) { + hash.splice(i, 1) + i-- + } + } + hash = hash.join('&') + location.hash = hash + if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() } + }) } } export function saveHistory (notehistory) { checkIfAuth( - () => { - saveHistoryToServer(notehistory) - }, - () => { - saveHistoryToStorage(notehistory) - } - ) + () => { + saveHistoryToServer(notehistory) + }, + () => { + saveHistoryToStorage(notehistory) + } + ) } function saveHistoryToStorage (notehistory) { @@ -79,9 +78,9 @@ export function saveStorageHistoryToServer (callback) { $.post(`${serverurl}/history`, { history: data }) - .done(data => { - callback(data) - }) + .done(data => { + callback(data) + }) } } @@ -108,7 +107,7 @@ export function clearDuplicatedHistory (notehistory) { } function addHistory (id, text, time, tags, pinned, notehistory) { - // only add when note id exists + // only add when note id exists if (id) { notehistory.push({ id, @@ -134,14 +133,14 @@ export function removeHistory (id, notehistory) { // used for inner export function writeHistory (title, tags) { checkIfAuth( - () => { - // no need to do this anymore, this will count from server-side - // writeHistoryToServer(title, tags); - }, - () => { - writeHistoryToStorage(title, tags) - } - ) + () => { + // no need to do this anymore, this will count from server-side + // writeHistoryToServer(title, tags); + }, + () => { + writeHistoryToStorage(title, tags) + } + ) } function writeHistoryToStorage (title, tags) { @@ -162,7 +161,7 @@ if (!Array.isArray) { } function renderHistory (title, tags) { - // console.debug(tags); + // console.debug(tags); const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1] return { id, @@ -174,7 +173,7 @@ function renderHistory (title, tags) { function generateHistory (title, tags, notehistory) { const info = renderHistory(title, tags) - // keep any pinned data + // keep any pinned data let pinned = false for (let i = 0; i < notehistory.length; i++) { if (notehistory[i].id === info.id && notehistory[i].pinned) { @@ -191,25 +190,25 @@ function generateHistory (title, tags, notehistory) { // used for outer export function getHistory (callback) { checkIfAuth( - () => { - getServerHistory(callback) - }, - () => { - getStorageHistory(callback) - } - ) + () => { + getServerHistory(callback) + }, + () => { + getStorageHistory(callback) + } + ) } function getServerHistory (callback) { $.get(`${serverurl}/history`) - .done(data => { - if (data.history) { - callback(data.history) - } - }) - .fail((xhr, status, error) => { - console.error(xhr.responseText) - }) + .done(data => { + if (data.history) { + callback(data.history) + } + }) + .fail((xhr, status, error) => { + console.error(xhr.responseText) + }) } export function getStorageHistory (callback) { @@ -224,25 +223,25 @@ export function getStorageHistory (callback) { export function parseHistory (list, callback) { checkIfAuth( - () => { - parseServerToHistory(list, callback) - }, - () => { - parseStorageToHistory(list, callback) - } - ) + () => { + parseServerToHistory(list, callback) + }, + () => { + parseStorageToHistory(list, callback) + } + ) } export function parseServerToHistory (list, callback) { $.get(`${serverurl}/history`) - .done(data => { - if (data.history) { - parseToHistory(list, data.history, callback) - } - }) - .fail((xhr, status, error) => { - console.error(xhr.responseText) - }) + .done(data => { + if (data.history) { + parseToHistory(list, data.history, callback) + } + }) + .fail((xhr, status, error) => { + console.error(xhr.responseText) + }) } export function parseStorageToHistory (list, callback) { @@ -268,15 +267,15 @@ function parseToHistory (list, notehistory, callback) { } catch (err) { console.error(err) } - // parse time to timestamp and fromNow + // parse time to timestamp and fromNow const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a')) notehistory[i].timestamp = timestamp.valueOf() notehistory[i].fromNow = timestamp.fromNow() notehistory[i].time = timestamp.format('llll') - // prevent XSS - notehistory[i].text = S(notehistory[i].text).escapeHTML().s - notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : [] - // add to list + // prevent XSS + notehistory[i].text = escapeHTML(notehistory[i].text) + notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHTML(notehistory[i].tags).split(',') : [] + // add to list if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) } } } diff --git a/public/js/index.js b/public/js/index.js index 76f952c1..366601c0 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,16 +1,7 @@ /* eslint-env browser, jquery */ -/* global CodeMirror, Cookies, moment, Spinner, Idle, serverurl, +/* global CodeMirror, Cookies, moment, Spinner, 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' @@ -20,58 +11,62 @@ import hljs from 'highlight.js' import _ from 'lodash' +import wurl from 'wurl' + import List from 'list.js' +import Idle from '@hackmd/idle-js' + import { - checkLoginStateChanged, - setloginStateChangeEvent + checkLoginStateChanged, + setloginStateChangeEvent } from './lib/common/login' import { - debug, - DROPBOX_APP_KEY, - noteid, - noteurl, - urlpath, - version + debug, + DROPBOX_APP_KEY, + noteid, + noteurl, + urlpath, + version } from './lib/config' import { - autoLinkify, - deduplicatedHeaderId, - exportToHTML, - exportToRawHTML, - removeDOMEvents, - finishView, - generateToc, - isValidURL, - md, - parseMeta, - postProcess, - renderFilename, - renderTOC, - renderTags, - renderTitle, - scrollToHash, - smoothHashScroll, - updateLastChange, - updateLastChangeUser, - updateOwner + autoLinkify, + deduplicatedHeaderId, + exportToHTML, + exportToRawHTML, + removeDOMEvents, + finishView, + generateToc, + isValidURL, + md, + parseMeta, + postProcess, + renderFilename, + renderTOC, + renderTags, + renderTitle, + scrollToHash, + smoothHashScroll, + updateLastChange, + updateLastChangeUser, + updateOwner } from './extra' import { - clearMap, - setupSyncAreas, - syncScrollToEdit, - syncScrollToView + clearMap, + setupSyncAreas, + syncScrollToEdit, + syncScrollToView } from './lib/syncscroll' import { - writeHistory, - deleteServerHistory, - getHistory, - saveHistory, - removeHistory + writeHistory, + deleteServerHistory, + getHistory, + saveHistory, + removeHistory } from './history' import { preventXSS } from './render' @@ -82,6 +77,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' @@ -418,7 +422,7 @@ Visibility.change(function (e, state) { $(document).ready(function () { idle.checkAway() checkResponsive() - // if in smaller screen, we don't need advanced scrollbar + // if in smaller screen, we don't need advanced scrollbar var scrollbarStyle if (visibleXS) { scrollbarStyle = 'native' @@ -438,12 +442,12 @@ $(document).ready(function () { if (isTouchDevice) { /* bind events */ $(document) - .on('focus', 'textarea, input', function () { - $body.addClass('fixfixed') - }) - .on('blur', 'textarea, input', function () { - $body.removeClass('fixfixed') - }) + .on('focus', 'textarea, input', function () { + $body.addClass('fixfixed') + }) + .on('blur', 'textarea, input', function () { + $body.removeClass('fixfixed') + }) } // Re-enable nightmode @@ -668,14 +672,14 @@ function checkEditorScrollbarInner () { } function checkTocStyle () { - // toc right + // toc right var paddingRight = parseFloat(ui.area.markdown.css('padding-right')) var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight)) ui.toc.toc.css('right', right + 'px') - // affix toc left + // affix toc left var newbool var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2 - // for ipad or wider device + // for ipad or wider device if (rightMargin >= 133) { newbool = true var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2 @@ -762,7 +766,7 @@ function toggleMode () { var lastMode = null function changeMode (type) { - // lock navbar to prevent it hide after changeMode + // lock navbar to prevent it hide after changeMode lockNavbar() saveInfo() if (type) { @@ -823,7 +827,7 @@ function changeMode (type) { } else { $(document.body).css('background-color', ui.area.codemirror.css('background-color')) } - // check resizable editor style + // check resizable editor style if (appState.currentMode === modeType.both) { if (lastEditorWidth > 0) { ui.area.edit.css('width', lastEditorWidth + 'px') @@ -901,13 +905,13 @@ function showMessageModal (title, header, href, text, success) { // check if dropbox app key is set and load scripts if (DROPBOX_APP_KEY) { $('