Full Code of hackmdio/codimd for AI

develop b1188a5648a2 cached
284 files
2.2 MB
600.8k tokens
679 symbols
1 requests
Download .txt
Showing preview only (2,397K chars total). Download the full file or copy to clipboard to get everything.
Repository: hackmdio/codimd
Branch: develop
Commit: b1188a5648a2
Files: 284
Total size: 2.2 MB

Directory structure:
gitextract_04c_nojh/

├── .buildpacks
├── .devcontainer/
│   ├── Dockerfile
│   ├── devcontainer.json
│   └── docker-compose.yml
├── .dockerignore
├── .editorconfig
├── .github/
│   ├── tests/
│   │   ├── README.md
│   │   └── pull-request.json
│   └── workflows/
│       ├── build.yml
│       ├── check-release.yml
│       └── push-image.yml
├── .gitignore
├── .mailmap
├── .nvmrc
├── .sequelizerc.example
├── AUTHORS
├── Aptfile
├── CONTRIBUTING.md
├── FUNDING.json
├── LICENSE
├── Procfile
├── README.md
├── app.js
├── app.json
├── babel.config.js
├── bin/
│   ├── heroku
│   ├── heroku_start.sh
│   ├── manage_users
│   └── setup
├── config.js
├── config.json.example
├── contribute/
│   └── developer-certificate-of-origin
├── deployments/
│   ├── Dockerfile
│   ├── build.sh
│   ├── docker-compose.yml
│   └── docker-entrypoint.sh
├── lib/
│   ├── auth/
│   │   ├── bitbucket/
│   │   │   └── index.js
│   │   ├── dropbox/
│   │   │   └── index.js
│   │   ├── email/
│   │   │   └── index.js
│   │   ├── facebook/
│   │   │   └── index.js
│   │   ├── github/
│   │   │   └── index.js
│   │   ├── gitlab/
│   │   │   └── index.js
│   │   ├── google/
│   │   │   └── index.js
│   │   ├── index.js
│   │   ├── ldap/
│   │   │   └── index.js
│   │   ├── mattermost/
│   │   │   └── index.js
│   │   ├── oauth2/
│   │   │   ├── index.js
│   │   │   └── strategy.js
│   │   ├── openid/
│   │   │   └── index.js
│   │   ├── saml/
│   │   │   └── index.js
│   │   ├── twitter/
│   │   │   └── index.js
│   │   └── utils.js
│   ├── config/
│   │   ├── default.js
│   │   ├── defaultSSL.js
│   │   ├── dockerSecret.js
│   │   ├── enum.js
│   │   ├── environment.js
│   │   ├── index.js
│   │   └── utils.js
│   ├── csp.js
│   ├── errorPage/
│   │   └── index.js
│   ├── history/
│   │   └── index.js
│   ├── homepage/
│   │   └── index.js
│   ├── imageRouter/
│   │   ├── azure.js
│   │   ├── filesystem.js
│   │   ├── imgur.js
│   │   ├── index.js
│   │   ├── lutim.js
│   │   ├── minio.js
│   │   └── s3.js
│   ├── letter-avatars.js
│   ├── logger.js
│   ├── metrics.js
│   ├── middleware/
│   │   ├── checkURIValid.js
│   │   ├── codiMDVersion.js
│   │   ├── redirectWithoutTrailingSlashes.js
│   │   └── tooBusy.js
│   ├── migrations/
│   │   ├── 20150504155329-create-users.js
│   │   ├── 20150508114741-create-notes.js
│   │   ├── 20150515125813-create-temp.js
│   │   ├── 20150702001020-update-to-0_3_1.js
│   │   ├── 20150915153700-change-notes-title-to-text.js
│   │   ├── 20160112220142-note-add-lastchange.js
│   │   ├── 20160420180355-note-add-alias.js
│   │   ├── 20160515114000-user-add-tokens.js
│   │   ├── 20160607060246-support-revision.js
│   │   ├── 20160703062241-support-authorship.js
│   │   ├── 20161009040430-support-delete-note.js
│   │   ├── 20161201050312-support-email-signin.js
│   │   ├── 20171009121200-longtext-for-mysql.js
│   │   ├── 20180209120907-longtext-of-authorship.js
│   │   ├── 20180306150303-fix-enum.js
│   │   ├── 20180326103000-use-text-in-tokens.js
│   │   ├── 20180525153000-user-add-delete-token.js
│   │   ├── 20200104215332-remove-temp-table.js
│   │   └── 20240114120250-revision-add-index.js
│   ├── models/
│   │   ├── author.js
│   │   ├── index.js
│   │   ├── note.js
│   │   ├── revision.js
│   │   └── user.js
│   ├── note/
│   │   ├── index.js
│   │   └── noteActions.js
│   ├── ot/
│   │   ├── client.js
│   │   ├── editor-socketio-server.js
│   │   ├── index.js
│   │   ├── selection.js
│   │   ├── server.js
│   │   ├── simple-text-operation.js
│   │   ├── text-operation.js
│   │   └── wrapped-operation.js
│   ├── realtime/
│   │   ├── processQueue.js
│   │   ├── realtime.js
│   │   ├── realtimeCleanDanglingUserJob.js
│   │   ├── realtimeClientConnection.js
│   │   ├── realtimeSaveRevisionJob.js
│   │   └── realtimeUpdateDirtyNoteJob.js
│   ├── response.js
│   ├── routes.js
│   ├── status/
│   │   └── index.js
│   ├── user/
│   │   └── index.js
│   ├── utils.js
│   ├── web/
│   │   └── middleware/
│   │       └── checkVersion.js
│   └── workers/
│       └── dmpWorker.js
├── locales/
│   ├── ca.json
│   ├── da.json
│   ├── de.json
│   ├── el.json
│   ├── en.json
│   ├── eo.json
│   ├── es.json
│   ├── fr.json
│   ├── hi.json
│   ├── hr.json
│   ├── id.json
│   ├── it.json
│   ├── ja.json
│   ├── ko.json
│   ├── nl.json
│   ├── pl.json
│   ├── pt.json
│   ├── ru.json
│   ├── sr.json
│   ├── sv.json
│   ├── tr.json
│   ├── uk.json
│   ├── zh-CN.json
│   └── zh-TW.json
├── package.json
├── public/
│   ├── .eslintrc.js
│   ├── css/
│   │   ├── bootstrap-social.css
│   │   ├── center.css
│   │   ├── codemirror-extend/
│   │   │   ├── ayu-dark.css
│   │   │   ├── ayu-mirage.css
│   │   │   ├── one-dark.css
│   │   │   ├── tomorrow-night-bright.css
│   │   │   └── tomorrow-night-eighties.css
│   │   ├── cover.css
│   │   ├── extra.css
│   │   ├── font.css
│   │   ├── github-extract.css
│   │   ├── google-font.css
│   │   ├── index.css
│   │   ├── markdown.css
│   │   ├── mermaid.css
│   │   ├── site.css
│   │   ├── slide-preview.css
│   │   └── slide.css
│   ├── default.md
│   ├── docs/
│   │   ├── features.md
│   │   ├── privacy.md.example
│   │   ├── release-notes.md
│   │   ├── slide-example.md
│   │   └── yaml-metadata.md
│   ├── js/
│   │   ├── cover.js
│   │   ├── extra.js
│   │   ├── history.js
│   │   ├── htmlExport.js
│   │   ├── index.js
│   │   ├── lib/
│   │   │   ├── appState.js
│   │   │   ├── common/
│   │   │   │   ├── constant.ejs
│   │   │   │   ├── login.js
│   │   │   │   └── metrics.ejs
│   │   │   ├── config/
│   │   │   │   └── index.js
│   │   │   ├── editor/
│   │   │   │   ├── config.js
│   │   │   │   ├── constants.js
│   │   │   │   ├── index.js
│   │   │   │   ├── markdown-lint/
│   │   │   │   │   └── index.js
│   │   │   │   ├── spellcheck.js
│   │   │   │   ├── statusbar.html
│   │   │   │   ├── table-editor.js
│   │   │   │   ├── toolbar.html
│   │   │   │   ├── ui-elements.js
│   │   │   │   └── utils.js
│   │   │   ├── markdown/
│   │   │   │   └── utils.js
│   │   │   ├── modeType.js
│   │   │   ├── renderer/
│   │   │   │   ├── csvpreview.js
│   │   │   │   ├── fretboard/
│   │   │   │   │   ├── css/
│   │   │   │   │   │   └── i.css
│   │   │   │   │   └── fretboard.js
│   │   │   │   └── lightbox/
│   │   │   │       ├── index.js
│   │   │   │       └── lightbox.css
│   │   │   └── syncscroll.js
│   │   ├── locale.js
│   │   ├── mathjax-config-extra.js
│   │   ├── pretty.js
│   │   ├── render.js
│   │   ├── reveal-markdown.js
│   │   ├── revealjs-plugins/
│   │   │   ├── elapsed-time-bar/
│   │   │   │   └── elapsed-time-bar.js
│   │   │   └── spotlight/
│   │   │       └── spotlight.js
│   │   ├── slide.js
│   │   └── utils.js
│   ├── markdown-lint/
│   │   └── css/
│   │       └── lint.css
│   ├── uploads/
│   │   └── .gitkeep
│   ├── vendor/
│   │   ├── abcjs_basic_3.1.1-min.js
│   │   ├── codemirror-spell-checker/
│   │   │   ├── en_US.aff
│   │   │   └── en_US.dic
│   │   ├── inlineAttachment/
│   │   │   ├── codemirror.inline-attachment.js
│   │   │   └── inline-attachment.js
│   │   ├── jquery-textcomplete/
│   │   │   └── jquery.textcomplete.js
│   │   ├── md-toc.js
│   │   ├── ot/
│   │   │   ├── ajax-adapter.js
│   │   │   ├── client.js
│   │   │   ├── codemirror-adapter.js
│   │   │   ├── compress.sh
│   │   │   ├── editor-client.js
│   │   │   ├── selection.js
│   │   │   ├── socketio-adapter.js
│   │   │   ├── text-operation.js
│   │   │   ├── undo-manager.js
│   │   │   └── wrapped-operation.js
│   │   └── showup/
│   │       ├── showup.css
│   │       └── showup.js
│   └── views/
│       ├── codimd/
│       │   ├── body.ejs
│       │   ├── foot.ejs
│       │   ├── footer.ejs
│       │   ├── head.ejs
│       │   └── header.ejs
│       ├── codimd.ejs
│       ├── error.ejs
│       ├── html.hbs
│       ├── includes/
│       │   ├── header.ejs
│       │   └── scripts.ejs
│       ├── index/
│       │   ├── body.ejs
│       │   ├── foot.ejs
│       │   ├── footer.ejs
│       │   ├── head.ejs
│       │   └── header.ejs
│       ├── index.ejs
│       ├── pretty.ejs
│       ├── shared/
│       │   ├── disqus.ejs
│       │   ├── ga.ejs
│       │   ├── help-modal.ejs
│       │   ├── pandoc-export-modal.ejs
│       │   ├── polyfill.ejs
│       │   ├── refresh-modal.ejs
│       │   ├── revision-modal.ejs
│       │   └── signin-modal.ejs
│       └── slide.ejs
├── scalingo.json
├── test/
│   ├── auth/
│   │   └── oauth2/
│   │       └── strategy.test.js
│   ├── connectionQueue.test.js
│   ├── csp.js
│   ├── letter-avatars.js
│   ├── realtime/
│   │   ├── cleanDanglingUser.test.js
│   │   ├── connection.test.js
│   │   ├── dirtyNoteUpdate.test.js
│   │   ├── disconnect-process.test.js
│   │   ├── extractNoteIdFromSocket.test.js
│   │   ├── ifMayEdit.test.js
│   │   ├── parseNoteIdFromSocket.test.js
│   │   ├── realtime.test.js
│   │   ├── saveRevisionJob.test.js
│   │   ├── socket-events.test.js
│   │   ├── updateNote.test.js
│   │   └── utils.js
│   └── testDoubles/
│       ├── ProcessQueueFake.js
│       ├── loggerFake.js
│       ├── otFake.js
│       └── realtimeJobStub.js
├── utils/
│   └── string.js
├── webpack.common.js
├── webpack.dev.js
├── webpack.htmlexport.js
└── webpack.prod.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .buildpacks
================================================
https://github.com/Scalingo/apt-buildpack
https://github.com/Scalingo/nodejs-buildpack


================================================
FILE: .devcontainer/Dockerfile
================================================
# [Choice] Node.js version: 16, 14
ARG VARIANT=14-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"

# [Optional] Uncomment if you want to install more global node modules
RUN su node -c "npm install -g npm@6"


================================================
FILE: .devcontainer/devcontainer.json
================================================
{
	"name": "CodiMD",
	"dockerComposeFile": "docker-compose.yml",
	"service": "app",
	"workspaceFolder": "/workspace",

	// Set *default* container specific settings.json values on container create.
	"settings": { 
		"terminal.integrated.shell.linux": "/bin/zsh",
		"sqltools.connections": [{
			"name": "Container Database",
			"driver": "PostgreSQL",
			"previewLimit": 50,
			"server": "localhost",
			"port": 5432,
			"database": "codimd",
			"username": "codimd",
			"password": "codimd"
		}],
	},

	// Add the IDs of extensions you want installed when the container is created.
	"extensions": [
		"dbaeumer.vscode-eslint",
		"visualstudioexptteam.vscodeintellicode",
		"christian-kohler.path-intellisense",
		"standard.vscode-standard",
		"mtxr.sqltools",
		"mtxr.sqltools-driver-pg",
		"eamodio.gitlens",
		"codestream.codestream", 
		"github.vscode-pull-request-github",
		"cschleiden.vscode-github-actions",
		"hbenl.vscode-mocha-test-adapter",
		"hbenl.vscode-test-explorer"
	],

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	"portsAttributes": {
		"3000": {
			"label": "CodiMD server",
			"onAutoForward": "notify"
		},
		"5432": {
			"label": "PostgreSQL",
			"onAutoForward": "notify"
		}
	},

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "yarn install",
	"postCreateCommand": "sudo chown -R node:node node_modules && /workspace/bin/setup",

	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
	"remoteUser": "node"
}

================================================
FILE: .devcontainer/docker-compose.yml
================================================
version: '3'

services: 
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
      args:
        VARIANT: 14-bullseye
    environment:
      - CMD_DB_URL=postgres://codimd:codimd@localhost/codimd
      - CMD_USECDN=false
    volumes: 
      - ..:/workspace:cached
      - node_modules:/workspace/node_modules:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
    network_mode: service:db

    # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.

    # Uncomment the next line to use a non-root user for all processes.
    # user: vscode

    # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)

  db:
    image: postgres:12.7-alpine
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=codimd
      - POSTGRES_PASSWORD=codimd
      - POSTGRES_DB=codimd

    # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)

volumes:
  node_modules:
  postgres-data: 

================================================
FILE: .dockerignore
================================================
.idea
coverage
node_modules/

# ignore config files
config.json
.sequelizerc

# ignore webpack build
public/build
public/views/build

.nyc_output
coverage/


================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

[{*.html,*.ejs}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[{.travis.yml,npm-shrinkwrap.json,package.json}]
indent_style = space
indent_size = 2


================================================
FILE: .github/tests/README.md
================================================
# Test github actions with act

```bash
act pull_request --container-architecture linux/arm64 -e .github/tests/pull-request.json -j ch
eck-release-pr -P ubuntu-latest=catthehacker/ubuntu:act-latest
```


================================================
FILE: .github/tests/pull-request.json
================================================
{
  "pull_request": {
    "head": {
      "ref": "release/1.2.3"
    },
    "base": {
      "ref": "master"
    }
  }
}


================================================
FILE: .github/workflows/build.yml
================================================
name: 'Test and Build'

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x]

    steps:
    - uses: actions/checkout@v4

    # from https://stackoverflow.com/a/69649733
    - name: Reconfigure git to use HTTP authentication
      run: >
        git config --global url."https://github.com/".insteadOf
        ssh://git@github.com/

    - uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-

    - uses: actions/setup-node@v4
      name: Use Node.js ${{ matrix.node-version }}
      with:
        node-version: ${{ matrix.node-version }}
        check-latest: true

    - run: npm ci
    - run: npm run test:ci
    - run: npm run build

  doctoc:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master' || github.event.pull_request

    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      name: Use Node.js 14
      with:
        node-version: 14
        check-latest: true
    - name: Install doctoc-check
      run: |
        npm install -g doctoc
        cp README.md README.md.orig
        npm run doctoc
        diff -q README.md README.md.orig


================================================
FILE: .github/workflows/check-release.yml
================================================
name: Release PR Checks

on:
  workflow_dispatch:
  pull_request:
    branches:
      - master

jobs:
  check-release-pr:
    if: startsWith(github.head_ref, 'release/')
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check for release-notes updates
        run: |
          if ! git diff --exit-code origin/develop -- public/docs/release-notes.md; then
            echo "Release notes updated."
          else
            echo "Error: Release notes not updated in the PR."
            exit 1
          fi

      - name: Compare package.json version with master
        run: |
          git fetch origin master
          MASTER_PACKAGE_VERSION=$(git show origin/master:package.json | jq -r '.version')
          BRANCH_PACKAGE_VERSION=$(jq -r '.version' package.json)

          if [ "$BRANCH_PACKAGE_VERSION" != "$MASTER_PACKAGE_VERSION" ]; then
            echo "Version bumped in package.json."
          else
            echo "Error: Version in package.json has not been bumped."
            exit 1
          fi



================================================
FILE: .github/workflows/push-image.yml
================================================
name: Build and push image

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      runtime:
        description: 'Runtime image'
        required: true
        default: 'hackmdio/runtime:16.20.2-35fe7e39'
      buildpack:
        description: 'Buildpack image'
        required: true
        default: 'hackmdio/buildpack:16.20.2-35fe7e39'

env:
  REGISTRY_IMAGE: hackmdio/hackmd

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        platform:
          - linux/amd64
          - linux/arm64
    steps:
      -
        name: Prepare
        run: |
          platform=${{ matrix.platform }}
          echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
      -
        name: Checkout
        uses: actions/checkout@v4
      -
        name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY_IMAGE }}
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push by digest
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./deployments/Dockerfile
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
          build-args: |
            RUNTIME=${{ github.event.inputs.runtime || 'hackmdio/runtime:16.20.2-35fe7e39' }}
            BUILDPACK=${{ github.event.inputs.buildpack || 'hackmdio/buildpack:16.20.2-35fe7e39' }}
      -
        name: Export digest
        run: |
          mkdir -p /tmp/digests
          digest="${{ steps.build.outputs.digest }}"
          touch "/tmp/digests/${digest#sha256:}"
      -
        name: Upload digest
        uses: actions/upload-artifact@v4
        with:
          name: digests-${{ env.PLATFORM_PAIR }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  merge:
    runs-on: ubuntu-latest
    needs:
      - build
    steps:
      -
        name: Download digests
        uses: actions/download-artifact@v4
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      -
        name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY_IMAGE }}
          tags: |
            type=match,pattern=\d.\d.\d
            type=sha,prefix=
      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Create manifest list and push
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
      -
        name: Inspect image
        run: |
          docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}


================================================
FILE: .gitignore
================================================
node_modules
composer.phar
composer.lock
.env.*.php
.env.php
.DS_Store
.idea/
Thumbs.db
npm-debug.log
hackmd_io
newrelic_agent.log
logs/
tmp/
backups/
*.pid
*.log
*.sqlite

# ignore config files
config.json
.sequelizerc

# ignore webpack build
public/build
public/views/build

public/uploads/*
!public/uploads/.gitkeep
/.nyc_output
/coverage/

.vscode/settings.json

================================================
FILE: .mailmap
================================================
Max Wu <jackymaxj@gmail.com>                 Wu Cheng-Han <jacky_cute0808@hotmail.com>
Max Wu <jackymaxj@gmail.com>                 Cheng-Han, Wu <jackymaxj@gmail.com>
Max Wu <jackymaxj@gmail.com>                 jackycute <jackymaxj@gmail.com>
Max Wu <jackymaxj@gmail.com>                 Wu, Cheng-Han <jackymaxj@gmail.com>
Max Wu <jackymaxj@gmail.com>                 jackycute <jacky_cute0808@hotmail.com>

Sheogorath <sheogorath@shivering-isles.com>  Christoph (Sheogorath) Kern <sheogorath@shivering-isles.com>

Raccoon <raccoon@hackmd.io>                  Raccoon Li <a60814billy@gmail.com>
Raccoon <raccoon@hackmd.io>                  Raccoon <a60814billy@gmail.com>

Peter Dave Hello <hsu@peterdavehello.org>    Peter Dave Hello <PeterDaveHello@users.noreply.github.com>

Claudius Coenen <github@amenthes.de>         Claudius Coenen <opensource@amenthes.de>


================================================
FILE: .nvmrc
================================================
v16.20.2


================================================
FILE: .sequelizerc.example
================================================
const path = require('path')
const config = require('./lib/config')

module.exports = {
  config: path.resolve('config.js'),
  'migrations-path': path.resolve('lib', 'migrations'),
  'models-path': path.resolve('lib', 'models'),
  url: config.dbURL
}


================================================
FILE: AUTHORS
================================================
alecdwm <alec@owls.io>
bananaappletw <bananaappletw@gmail.com>
Bartlomiej Szala <fenix440@gmail.com>
BoHong Li <a60814billy@gmail.com>
Bryan Davis <bd808@wikimedia.org>
butlerx <butlerx@notthe.cloud>
Cheng-Han, Wu <jackymaxj@gmail.com>
Christian Schuhmann <madebyherzblut@users.noreply.github.com>
Colin Maudry <colin@maudry.com>
Dmytro Kytsmen <dmitrokytsmen@gmail.com>
Fabien Meghazi <agr@amigrave.com>
Florian Rhiem <florian.rhiem@gmail.com>
geekyd <singhsince94@gmail.com>
GhiMax <ghina8@gmail.com>
greenkeeperio-bot <support@greenkeeper.io>
Himura Kazuto <Himura2la@users.noreply.github.com>
Ho33e5 <ho33e5@gmail.com>
Ian Dees <ian.dees@gmail.com>
Ikumi Shimizu <193s@users.noreply.github.com>
ivanorsolic <ivanorsolic@users.noreply.github.com>
jackycute <jacky_cute0808@hotmail.com>
jackycute <jackymaxj@gmail.com>
Jakub Sygnowski <sygnowski@gmail.com>
James Stephenson <c4p7.fl1n7@gmail.com>
Jan Kunzmann <jan-github@phobia.de>
Jannik Lorenz <dev@janniklorenz.de>
Jason Croft <jcroft@velocity.org>
Johannes Weißl <jargon@molb.org>
Jordan Matelsky <j6k4m8@gmail.com>
Jun SAKATA <jun.bj141400@gmail.com>
Kaiyu Shi <skyisno.1@gmail.com>
knjcode <knjcode@gmail.com>
Kotaro Yamamoto <kota.crk@gmail.com>
Lars Karlsson <lars@kajes.se>
Laura Kyle <laura.kyle91@gmail.com>
LluisArevalo <thorin119@gmail.com>
Marcelo Alencar <marceloalves@ufpa.br>
Martijnpold <martijntje7@gmail.com>
Max Wu <jackymaxj@gmail.com>
neopostmodern <clemens@neopostmodern.com>
NV <nvsofts@gmail.com>
Ömer Erdinç Yağmurlu <omeryagmurlu@gmail.com>
p0v1n0m <p0v1n0m@gmail.com>
Pablo Guerrero <pablo.guerrero@gmail.com>
Pablo Guerrero <pablo.guerrero@sap.com>
Paras <paraschadha2052@gmail.com>
Patrick Andersen <patrick@bacha.dk>
Peter Dave Hello <hsu@peterdavehello.org>
Peter Dave Hello <PeterDaveHello@users.noreply.github.com>
Philipp Zumstein <zuphilip@users.noreply.github.com>
Raccoon Li <a60814billy@gmail.com>
robert <ahmerov.rt@molodost.bz>
Sergio Valverde <svg153@users.noreply.github.com>
Sheogorath <sheogorath@shivering-isles.com>
Simon Joda Stößer <SimJoSt@users.noreply.github.com>
S.Noda <noda@fenrir.co.jp>
Stratos Gerakakis <stratosgear@gmail.com>
The Gitter Badger <badger@gitter.im>
tkqubo <tk.qubo@gmail.com>
tkykm <tkykm@users.noreply.github.com>
Tom Wyckhuys <tomwyckhuys@gmail.com>
Wonder Chang <iwonder.tw@gmail.com>
Wu Cheng-Han <jacky_cute0808@hotmail.com>
Xavier Marques <xaviermarques4f@gmail.com>
xnum <s000032001@gmail.com>
Yukai Huang <yukaihuangtw@gmail.com>
zachariast <zachariastraianos@gmail.com>
Zankio <xxoojoeooxx1@gmail.com>
蒼時弦也 <elct9620@frost.tw>


================================================
FILE: Aptfile
================================================
libvips-dev


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

Please note we have a code of conduct, please follow it in all your interactions with the project.

## Pull Request Process
1. Ensure you signed all your commits with Developer Certificate of Origin (DCO).
2. Ensure any install or build dependencies are removed before the end of the layer when doing a
   build.
3. Update the README.md with details of changes to the interface, this includes new environment
   variables, exposed ports, useful file locations and container parameters.
4. Increase the version numbers in any examples files and the README.md to the new version that this
   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
   do not have permission to do that, you may request the second reviewer to merge it for you.

## Contributor Code of Conduct

As contributors and maintainers of this project, and in the interest of fostering an open and
welcoming community, we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation, submitting pull requests or patches, and other
activities.

We are committed to making participation in this project a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression, sexual orientation,
disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit
  permission
* Other unethical or unprofessional conduct.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits,
code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By
adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project. Project maintainers who do not
follow or enforce the Code of Conduct may be permanently removed from the project team.

This code of conduct applies both within project spaces and in public spaces when an individual is
representing the project or its community.

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an
issue or contacting one or more of the project maintainers.

This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org),
version 1.2.0, available at
[http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)

### Sign your work

We use the Developer Certificate of Origin (DCO) as a additional safeguard
for the CodiMD project. This is a well established and widely used
mechanism to assure contributors have confirmed their right to license
their contribution under the project's license.
Please read [contribute/developer-certificate-of-origin][dcofile].
If you can certify it, then just add a line to every git commit message:

````
  Signed-off-by: Random J Developer <random@developer.example.org>
````

Use your real name (sorry, no pseudonyms or anonymous contributions).
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`. You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases)
like `git config --global alias.ci 'commit -s'`. Now you can commit with
`git ci` and the commit will be signed.

[dcofile]: https://github.com/hackmdio/codimd/blob/develop/contribute/developer-certificate-of-origin


================================================
FILE: FUNDING.json
================================================
{
  "drips": {
    "ethereum": {
      "ownedBy": "0xEd37B84FD84A834886aC07693aF6A9cd35040002"
    }
  }
}


================================================
FILE: LICENSE
================================================
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

================================================
FILE: Procfile
================================================
web: ./bin/heroku_start.sh


================================================
FILE: README.md
================================================
CodiMD
===

[![build status][build-image]][build-url]
[![version][github-version-badge]][github-release-page]
[![Gitter][gitter-image]][gitter-url]
[![Matrix][matrix-image]][matrix-url]
[![POEditor][poeditor-image]][poeditor-url]

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.

![screenshot](https://raw.githubusercontent.com/hackmdio/codimd/develop/public/screenshot.png)

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Table of Contents

- [HackMD](#hackmd)
- [CodiMD - The Open Source HackMD](#codimd---the-open-source-hackmd)
- [Documentation](#documentation)
  - [Deployment](#deployment)
  - [Configuration](#configuration)
  - [Upgrading and Migration](#upgrading-and-migration)
  - [Developer](#developer)
- [Contribution and Discussion](#contribution-and-discussion)
- [Browser Support](#browser-support)
- [License](#license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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/).

## CodiMD - The Open Source HackMD

CodiMD is the free software version of [HackMD](https://hackmd.io), developed and open sourced by the HackMD team with reduced features (without book mode), you can use CodiMD for your community and own all your data. *(See the [origin of the name CodiMD](https://github.com/hackmdio/hackmd/issues/720).)* 

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!

## Documentation
You would find all documentation here: [CodiMD Documentation](https://hackmd.io/c/codimd-documentation)

### Deployment
If you want to spin up an instance and start using immediately, see [Docker deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-docker-deployment).
If you want to contribute to the project, start with [manual deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment).

### 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).

### Upgrading and Migration
Upgrade CodiMD from previous version? See [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-upgrade)<br>
Migrating from Etherpad? Follow [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-migration-etherpad)

### 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.

## Contribution and Discussion
All contributions are welcome! Even asking a question helps.

| Project | Contribution Types | Contribution Venue |
| ------- | ------------------ | ------------------ |
|**CodiMD**|:couple: Community chat|[Gitter][gitter-url]|
||: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][poeditor-url]|
||:coffee: Donation|[Buy us coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=KDGS4PREHX6QQ&lc=US&item_name=HackMD&currency_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: 
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" /> Chrome >= 47, Chrome for Android >= 47
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" /> Safari >= 9, iOS Safari >= 8.4
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" /> Firefox >= 44
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" /> Edge >= 12
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" /> Opera >= 34, Opera Mini not supported
- Android Browser >= 4.4

To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed].

## License

**License under AGPL.**

[gitter-image]: https://img.shields.io/badge/gitter-hackmdio/codimd-blue.svg
[gitter-url]: https://gitter.im/hackmdio/hackmd
[build-image]: https://github.com/hackmdio/codimd/actions/workflows/build.yml/badge.svg
[build-url]: https://github.com/hackmdio/codimd/actions/workflows/build.yml
[github-version-badge]: https://img.shields.io/github/release/hackmdio/codimd.svg
[github-release-page]: https://github.com/hackmdio/codimd/releases
[github-release-feed]: https://github.com/hackmdio/codimd/releases.atom
[poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg
[poeditor-url]: https://poeditor.com/join/project/q0nuPWyztp
[matrix-image]: https://img.shields.io/matrix/hackmdio_hackmd:gitter.im?color=blue&logo=matrix
[matrix-url]: https://matrix.to/#/#hackmdio_hackmd:gitter.im


================================================
FILE: app.js
================================================
'use strict'
// app
// external modules
var express = require('express')

var ejs = require('ejs')
var passport = require('passport')
var methodOverride = require('method-override')
var cookieParser = require('cookie-parser')
var session = require('express-session')
var SequelizeStore = require('connect-session-sequelize')(session.Store)
var fs = require('fs')
var path = require('path')

var morgan = require('morgan')
var passportSocketIo = require('passport.socketio')
var helmet = require('helmet')
var i18n = require('i18n')
var flash = require('connect-flash')
var apiMetrics = require('prometheus-api-metrics')

// core
var config = require('./lib/config')
var logger = require('./lib/logger')
var response = require('./lib/response')
var models = require('./lib/models')
var csp = require('./lib/csp')
const { Environment } = require('./lib/config/enum')

const { versionCheckMiddleware, checkVersion } = require('./lib/web/middleware/checkVersion')

function createHttpServer () {
  if (config.useSSL) {
    const ca = (function () {
      let i, len
      const results = []
      for (i = 0, len = config.sslCAPath.length; i < len; i++) {
        results.push(fs.readFileSync(config.sslCAPath[i], 'utf8'))
      }
      return results
    })()
    const options = {
      key: fs.readFileSync(config.sslKeyPath, 'utf8'),
      cert: fs.readFileSync(config.sslCertPath, 'utf8'),
      ca: ca,
      dhparam: fs.readFileSync(config.dhParamPath, 'utf8'),
      requestCert: false,
      rejectUnauthorized: false
    }
    return require('https').createServer(options, app)
  } else {
    return require('http').createServer(app)
  }
}

// server setup
var app = express()
var server = createHttpServer()

// API and process monitoring with Prometheus for Node.js micro-service
app.use(apiMetrics({
  metricsPath: '/metrics/router',
  excludeRoutes: ['/metrics/codimd']
}))

// logger
app.use(morgan('combined', {
  stream: logger.stream
}))

// socket io
var io = require('socket.io')(server)
io.engine.ws = new (require('ws').Server)({
  noServer: true,
  perMessageDeflate: false
})

// others
var realtime = require('./lib/realtime/realtime.js')

// assign socket io to realtime
realtime.io = io

// methodOverride
app.use(methodOverride('_method'))

// session store
var sessionStore = new SequelizeStore({
  db: models.sequelize
})

// use hsts to tell https users stick to this
if (config.hsts.enable) {
  app.use(helmet.hsts({
    maxAge: config.hsts.maxAgeSeconds,
    includeSubdomains: config.hsts.includeSubdomains,
    preload: config.hsts.preload
  }))
} else if (config.useSSL) {
  logger.info('Consider enabling HSTS for extra security:')
  logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security')
}

// Add referrer policy to improve privacy
app.use(
  helmet.referrerPolicy({
    policy: 'same-origin'
  })
)

// Generate a random nonce per request, for CSP with inline scripts
app.use(csp.addNonceToLocals)

// use Content-Security-Policy to limit XSS, dangerous plugins, etc.
// https://helmetjs.github.io/docs/csp/
if (config.csp.enable) {
  app.use(helmet.contentSecurityPolicy({
    directives: csp.computeDirectives()
  }))
} else {
  logger.info('Content-Security-Policy is disabled. This may be a security risk.')
}

i18n.configure({
  locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr'],
  cookie: 'locale',
  directory: path.join(__dirname, '/locales'),
  updateFiles: config.updateI18nFiles
})

app.use(cookieParser())

app.use(i18n.init)

// routes without sessions
// static files
app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticCacheTime, index: false }))
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), { maxAge: config.staticCacheTime }))
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), { maxAge: config.staticCacheTime }))
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), { maxAge: config.staticCacheTime }))
app.use(require('./lib/metrics').router)

// session
app.use(session({
  name: config.sessionName,
  secret: config.sessionSecret,
  resave: false, // don't save session if unmodified
  saveUninitialized: true, // always create session to ensure the origin
  rolling: true, // reset maxAge on every response
  cookie: {
    maxAge: config.sessionLife
  },
  store: sessionStore
}))

// session resumption
var tlsSessionStore = {}
server.on('newSession', function (id, data, cb) {
  tlsSessionStore[id.toString('hex')] = data
  cb()
})
server.on('resumeSession', function (id, cb) {
  cb(null, tlsSessionStore[id.toString('hex')] || null)
})

// middleware which blocks requests when we're too busy
app.use(require('./lib/middleware/tooBusy'))

app.use(flash())

// passport
app.use(passport.initialize())
app.use(passport.session())

// check uri is valid before going further
app.use(require('./lib/middleware/checkURIValid'))
// redirect url without trailing slashes
app.use(require('./lib/middleware/redirectWithoutTrailingSlashes'))
app.use(require('./lib/middleware/codiMDVersion'))

if (config.autoVersionCheck && process.env.NODE_ENV === Environment.production) {
  checkVersion(app)
  app.use(versionCheckMiddleware)
}

// routes need sessions
// template files
app.set('views', config.viewPath)
// set render engine
app.engine('ejs', ejs.renderFile)
// set view engine
app.set('view engine', 'ejs')
// set generally available variables for all views
app.locals.useCDN = config.useCDN
app.locals.serverURL = config.serverURL
app.locals.sourceURL = config.sourceURL
app.locals.privacyPolicyURL = config.privacyPolicyURL
app.locals.allowAnonymous = config.allowAnonymous
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
app.locals.permission = config.permission
app.locals.allowPDFExport = config.allowPDFExport
app.locals.authProviders = {
  facebook: config.isFacebookEnable,
  twitter: config.isTwitterEnable,
  github: config.isGitHubEnable,
  bitbucket: config.isBitbucketEnable,
  gitlab: config.isGitLabEnable,
  mattermost: config.isMattermostEnable,
  dropbox: config.isDropboxEnable,
  google: config.isGoogleEnable,
  ldap: config.isLDAPEnable,
  ldapProviderName: config.ldap.providerName,
  saml: config.isSAMLEnable,
  oauth2: config.isOAuth2Enable,
  oauth2ProviderName: config.oauth2.providerName,
  openID: config.isOpenIDEnable,
  email: config.isEmailEnable,
  allowEmailRegister: config.allowEmailRegister
}
app.locals.versionInfo = {
  latest: true,
  versionItem: null
}

// Export/Import menu items
app.locals.enableDropBoxSave = config.isDropboxEnable
app.locals.enableGitHubGist = config.isGitHubEnable
app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable

app.use(require('./lib/routes').router)

// response not found if no any route matxches
app.get('*', function (req, res) {
  response.errorNotFound(req, res)
})

// socket.io secure
io.use(realtime.secure)
// socket.io auth
io.use(passportSocketIo.authorize({
  cookieParser: cookieParser,
  key: config.sessionName,
  secret: config.sessionSecret,
  store: sessionStore,
  success: realtime.onAuthorizeSuccess,
  fail: realtime.onAuthorizeFail
}))
// socket.io heartbeat
io.set('heartbeat interval', config.heartbeatInterval)
io.set('heartbeat timeout', config.heartbeatTimeout)
// socket.io connection
io.sockets.on('connection', realtime.connection)

// listen
function startListen () {
  var address
  var listenCallback = function () {
    var schema = config.useSSL ? 'HTTPS' : 'HTTP'
    logger.info('%s Server listening at %s', schema, address)
    realtime.maintenance = false
  }

  // use unix domain socket if 'path' is specified
  if (config.path) {
    address = config.path
    server.listen(config.path, listenCallback)
  } else {
    address = config.host + ':' + config.port
    server.listen(config.port, config.host, listenCallback)
  }
}

// sync db then start listen
models.sequelize.sync().then(function () {
  // check if realtime is ready
  if (realtime.isReady()) {
    models.Revision.checkAllNotesRevision(function (err, notes) {
      if (err) throw new Error(err)
      if (!notes || notes.length <= 0) return startListen()
    })
  } else {
    throw new Error('server still not ready after db synced')
  }
}).catch(err => {
  logger.error('Can\'t sync database')
  logger.error(err.stack)
  logger.error('Process will exit now.')
  process.exit(1)
})

// log uncaught exception
process.on('uncaughtException', function (err) {
  logger.error('An uncaught exception has occured.')
  logger.error(err)
  console.error(err)
  logger.error('Process will exit now.')
  process.exit(1)
})

// install exit handler
function handleTermSignals () {
  logger.info('CodiMD has been killed by signal, try to exit gracefully...')
  realtime.maintenance = true
  realtime.terminate()
  // disconnect all socket.io clients
  Object.keys(io.sockets.sockets).forEach(function (key) {
    var socket = io.sockets.sockets[key]
    // notify client server going into maintenance status
    socket.emit('maintenance')
    setTimeout(function () {
      socket.disconnect(true)
    }, 0)
  })
  var checkCleanTimer = setInterval(function () {
    if (realtime.isReady()) {
      models.Revision.checkAllNotesRevision(function (err, notes) {
        if (err) return logger.error(err)
        if (!notes || notes.length <= 0) {
          clearInterval(checkCleanTimer)
          return process.exit(0)
        }
      })
    }
  }, 100)
  setTimeout(() => {
    process.exit(1)
  }, 5000)
}
process.on('SIGINT', handleTermSignals)
process.on('SIGTERM', handleTermSignals)
process.on('SIGQUIT', handleTermSignals)


================================================
FILE: app.json
================================================
{
    "name": "CodiMD",
    "description": "Realtime collaborative markdown notes on all platforms",
    "keywords": [
        "Collaborative",
        "Markdown",
        "Notes"
    ],
    "website": "https://github.com/hackmdio/codimd",
    "repository": "https://github.com/hackmdio/codimd",
    "logo": "https://github.com/hackmdio/codimd/raw/master/public/codimd-icon-1024.png",
    "success_url": "/",
    "env": {
        "NPM_CONFIG_PRODUCTION": {
            "description": "Let npm also install development build tool",
            "value": "false"
        },
        "CMD_SESSION_SECRET": {
            "description": "Secret used to secure session cookies.",
            "required": false
        },
        "CMD_HSTS_ENABLE": {
            "description": "whether to also use HSTS if HTTPS is enabled",
            "required": false
        },
        "CMD_HSTS_MAX_AGE": {
            "description": "max duration, in seconds, to tell clients to keep HSTS status",
            "required": false
        },
        "CMD_HSTS_INCLUDE_SUBDOMAINS": {
            "description": "whether to tell clients to also regard subdomains as HSTS hosts",
            "required": false
        },
        "CMD_HSTS_PRELOAD": {
            "description": "whether to allow at all adding of the site to HSTS preloads (e.g. in browsers)",
            "required": false
        },
        "CMD_DOMAIN": {
            "description": "domain name",
            "required": false
        },
        "CMD_URL_PATH": {
            "description": "sub url path, like `www.example.com/<URL_PATH>`",
            "required": false
        },
        "CMD_ALLOW_ORIGIN": {
            "description": "domain name whitelist (use comma to separate)",
            "required": false,
            "value": "localhost"
        },
        "CMD_PROTOCOL_USESSL": {
            "description": "set to use ssl protocol for resources path (only applied when domain is set)",
            "required": false
        },
        "CMD_URL_ADDPORT": {
            "description": "set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set)",
            "required": false
        },
        "CMD_FACEBOOK_CLIENTID": {
            "description": "Facebook API client id",
            "required": false
        },
        "CMD_FACEBOOK_CLIENTSECRET": {
            "description": "Facebook API client secret",
            "required": false
        },
        "CMD_TWITTER_CONSUMERKEY": {
            "description": "Twitter API consumer key",
            "required": false
        },
        "CMD_TWITTER_CONSUMERSECRET": {
            "description": "Twitter API consumer secret",
            "required": false
        },
        "CMD_GITHUB_CLIENTID": {
            "description": "GitHub API client id",
            "required": false
        },
        "CMD_GITHUB_CLIENTSECRET": {
            "description": "GitHub API client secret",
            "required": false
        },
        "CMD_GITHUB_ORGANIZATIONS": {
            "description": "GitHub whitelist of orgs",
            "required": false
        },
        "CMD_GITHUB_SCOPES": {
            "description": "GitHub OAuth API scopes",
            "required": false
        },
        "CMD_BITBUCKET_CLIENTID": {
            "description": "Bitbucket API client id",
            "required": false
        },
        "CMD_BITBUCKET_CLIENTSECRET": {
            "description": "Bitbucket API client secret",
            "required": false
        },
        "CMD_GITLAB_BASEURL": {
            "description": "GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional)",
            "required": false
        },
        "CMD_GITLAB_CLIENTID": {
            "description": "GitLab API client id",
            "required": false
        },
        "CMD_GITLAB_CLIENTSECRET": {
            "description": "GitLab API client secret",
            "required": false
        },
        "CMD_GITLAB_SCOPE": {
            "description": "GitLab API client scope (optional)",
            "required": false
        },
        "CMD_MATTERMOST_BASEURL": {
            "description": "Mattermost authentication endpoint",
            "required": false
        },
        "CMD_MATTERMOST_CLIENTID": {
            "description": "Mattermost API client id",
            "required": false
        },
        "CMD_MATTERMOST_CLIENTSECRET": {
            "description": "Mattermost API client secret",
            "required": false
        },
        "CMD_DROPBOX_CLIENTID": {
            "description": "Dropbox API client id",
            "required": false
        },
        "CMD_DROPBOX_CLIENTSECRET": {
            "description": "Dropbox API client secret",
            "required": false
        },
        "CMD_DROPBOX_APP_KEY": {
            "description": "Dropbox app key (for import/export)",
            "required": false
        },
        "CMD_GOOGLE_CLIENTID": {
            "description": "Google API client id",
            "required": false
        },
        "CMD_GOOGLE_CLIENTSECRET": {
            "description": "Google API client secret",
            "required": false
        },
        "CMD_IMGUR_CLIENTID": {
            "description": "Imgur API client id",
            "required": false
        },
        "CMD_ALLOW_PDF_EXPORT": {
            "description": "Enable or disable PDF exports",
            "required": false
        },
        "PGSSLMODE": {
          "description": "Enforce PG SSL mode",
          "value": "require"
        }
    },
    "addons": [
        "heroku-postgresql"
    ],
    "buildpacks": [
        {
            "url": "https://github.com/alex88/heroku-buildpack-vips"
        },
        {
            "url": "https://github.com/heroku/heroku-buildpack-nodejs"
        }
    ]
}


================================================
FILE: babel.config.js
================================================
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: '14'
      },
      useBuiltIns: 'usage',
      corejs: 3,
      modules: 'auto'
    }]
  ],
  plugins: [
    ['@babel/plugin-transform-runtime', {
      corejs: 3
    }],
    '@babel/plugin-transform-nullish-coalescing-operator',
    '@babel/plugin-transform-optional-chaining'
  ]
}


================================================
FILE: bin/heroku
================================================
#!/bin/bash

set -e

if [ ! -z "$DYNO" ]; then
  # setup config files
  cp .sequelizerc.example .sequelizerc

    cat << EOF > config.json

{
  "production": {
  }
}

EOF

fi


================================================
FILE: bin/heroku_start.sh
================================================
#!/bin/bash

set -euo pipefail

CMD_DB_URL="$DATABASE_URL" CMD_PORT="$PORT" npm run start


================================================
FILE: bin/manage_users
================================================
#!/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 models = require('../lib/models/')
const readline = require('readline-sync')
const minimist = require('minimist')

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\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']
}

// Using an async function to be able to use await inside
async function createUser (argv) {
  const existingUser = await models.User.findOne({ where: { email: argv['add'] } })
  // Cannot create already-existing users
  if (existingUser) {
    console.log(`User with e-mail ${existingUser.email} already exists! Aborting ...`)
    process.exit(2)
  }

  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']}`) }
}

// Using an async function to be able to use await inside
async function deleteUser (argv) {
  // Cannot delete non-existing users
  const existingUser = await models.User.findOne({ where: { email: argv['del'] } })
  if (!existingUser) {
    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 existingUser.destroy()
  console.log(`Deleted user ${argv['del']} ...`)
}

// Using an async function to be able to use await inside
async function resetUser (argv) {
  const existingUser = await models.User.findOne({ where: { email: argv['reset'] } })
  // Cannot reset non-existing users
  if (!existingUser) {
    console.log(`User with e-mail ${argv['reset']} does not exist, cannot reset`)
    process.exit(1)
  }

  const pass = getPass(argv, '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
}

// Perform commandline-parsing
const argv = minimist(process.argv.slice(2))

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 ')}!`)
}

// Check if both are specified
if (opts.length > 1) {
  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}`)
}

// Call respective processing functions
options[action](argv).then(function () {
  process.exit(0)
})


================================================
FILE: bin/setup
================================================
#!/bin/bash

set -e

# run command at repo root
CURRENT_PATH=$PWD
if [ -d .git ]; then
  cd "$(git rev-parse --show-toplevel)"
fi

if ! type npm > /dev/null
then
  cat << EOF
npm is not installed, please install Node.js and npm.
Read more on Node.js official website: https://nodejs.org
Setup will not be run
EOF
  exit 0
fi

echo "copy config files"
if [ ! -f config.json ]; then
  cp config.json.example config.json
fi

if [ ! -f .sequelizerc ]; then
  cp .sequelizerc.example .sequelizerc
fi

echo "install packages"
npm install

cat << EOF


Edit the following config file to setup CodiMD server and client.
Read more info at https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration

* config.json           -- CodiMD config
* .sequelizerc          -- db config

EOF

# change directory back
cd "$CURRENT_PATH"


================================================
FILE: config.js
================================================
const config = require('./lib/config')

module.exports = config.db


================================================
FILE: config.json.example
================================================
{
    "test": {
        "db": {
            "dialect": "sqlite",
            "storage": ":memory:"
        },
        "linkifyHeaderStyle": "gfm"
    },
    "development": {
        "loglevel": "debug",
        "hsts": {
            "enable": false
        },
        "db": {
            "dialect": "sqlite",
            "storage": "./db.codimd.sqlite"
        },
        "linkifyHeaderStyle": "gfm"
    },
    "production": {
        "domain": "localhost",
        "loglevel": "info",
        "hsts": {
            "enable": true,
            "maxAgeSeconds": 31536000,
            "includeSubdomains": true,
            "preload": true
        },
        "csp": {
            "enable": true,
            "directives": {
            },
            "upgradeInsecureRequests": "auto",
            "addDefaults": true,
            "addDisqus": true,
            "addGoogleAnalytics": true
        },
        "db": {
            "username": "",
            "password": "",
            "database": "codimd",
            "host": "localhost",
            "port": "5432",
            "dialect": "postgres"
        },
        "facebook": {
            "clientID": "change this",
            "clientSecret": "change this"
        },
        "twitter": {
            "consumerKey": "change this",
            "consumerSecret": "change this"
        },
        "github": {
            "clientID": "change this",
            "clientSecret": "change this",
            "organizations": ["names of github organizations allowed, optional"],
            "scopes": ["defaults to 'read:user' scope for auth user"]
        },
        "gitlab": {
            "baseURL": "change this",
            "clientID": "change this",
            "clientSecret": "change this",
            "scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')",
            "version": "use 'v4' if gitlab version > 11, 'v3' otherwise. Default to 'v4'"
        },
        "mattermost": {
            "baseURL": "change this",
            "clientID": "change this",
            "clientSecret": "change this"
        },
        "dropbox": {
            "clientID": "change this",
            "clientSecret": "change this",
            "appKey": "change this"
        },
        "google": {
            "clientID": "change this",
            "clientSecret": "change this",
            "apiKey": "change this"
        },
        "ldap": {
            "url": "ldap://change_this",
            "bindDn": null,
            "bindCredentials": null,
            "searchBase": "change this",
            "searchFilter": "change this",
            "searchAttributes": ["change this"],
            "usernameField": "change this e.g. cn",
            "useridField": "change this e.g. uid",
            "tlsOptions": {
                "changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback"
            }
        },
        "saml": {
            "idpSsoUrl": "change: authentication endpoint of IdP",
            "idpCert": "change: certificate file path of IdP in PEM format",
            "issuer": "change or delete: identity of the service provider (default: serverurl)",
            "identifierFormat": "change or delete: name identifier format (default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')",
            "disableRequestedAuthnContext": "change or delete: true to allow any authentication method, false restricts to password authentication method (default: false)",
            "groupAttribute": "change or delete: attribute name for group list (ex: memberOf)",
            "requiredGroups": [ "change or delete: group names that allowed" ],
            "externalGroups": [ "change or delete: group names that not allowed" ],
            "attribute": {
               "id": "change or delete this: attribute map for `id` (default: NameID)",
               "username": "change or delete this: attribute map for `username` (default: NameID)",
               "email": "change or delete this: attribute map for `email` (default: NameID)"
            }
        },
        "imgur": {
            "clientID": "change this"
        },
        "minio": {
          "accessKey": "change this",
          "secretKey": "change this",
          "endPoint": "change this",
          "secure": true,
          "port": 9000
        },
        "s3": {
          "accessKeyId": "change this",
          "secretAccessKey": "change this",
          "region": "change this"
        },
        "s3bucket": "change this",
        "azure":
        {
          "connectionString": "change this",
          "container": "change this"
        },
        "plantuml":
        {
          "server": "https://www.plantuml.com/plantuml"
        },
        "linkifyHeaderStyle": "gfm"
    }
}


================================================
FILE: contribute/developer-certificate-of-origin
================================================
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.


================================================
FILE: deployments/Dockerfile
================================================
ARG RUNTIME
ARG BUILDPACK

FROM $BUILDPACK as BUILD

COPY --chown=hackmd:hackmd . .
ENV QT_QPA_PLATFORM=offscreen

RUN set -xe && \
    git reset --hard && \
    git clean -fx && \
    npm install && \
    npm run build && \
    cp ./deployments/docker-entrypoint.sh ./ && \
    cp .sequelizerc.example .sequelizerc && \
    rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \
        test docs contribute \
        package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
        config.json.example README.md CONTRIBUTING.md AUTHORS node_modules

FROM $RUNTIME
USER hackmd
ENV QT_QPA_PLATFORM=offscreen
WORKDIR /home/hackmd/app
COPY --chown=1500:1500 --from=BUILD /home/hackmd/app .
RUN npm install --production && npm cache clean --force && rm -rf /tmp/{core-js-banners,phantomjs}
EXPOSE 3000
ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"]


================================================
FILE: deployments/build.sh
================================================
#!/usr/bin/env bash

set -eo pipefail
set -x

if [[ -z $1 || -z $2 ]];then
    echo "build.sh [runtime image] [buildpack image]"
    exit 1
fi

CURRENT_DIR=$(dirname "$BASH_SOURCE")

GIT_SHA1="$(git rev-parse HEAD)"
GIT_SHORT_ID="${GIT_SHA1:0:8}"
GIT_TAG=$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || echo "")

DOCKER_TAG="${GIT_TAG:-$GIT_SHORT_ID}"

docker build --build-arg RUNTIME=$1 --build-arg BUILDPACK=$2 -t "hackmdio/hackmd:$DOCKER_TAG" -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."



================================================
FILE: deployments/docker-compose.yml
================================================
version: "3"
services:
  database:
    image: postgres:11.6-alpine
    environment:
      - POSTGRES_USER=codimd
      - POSTGRES_PASSWORD=change_password
      - POSTGRES_DB=codimd
    volumes:
      - "database-data:/var/lib/postgresql/data"
    restart: always
  codimd:
    # you can use image or custom build below,
    image: nabo.codimd.dev/hackmdio/hackmd:2.5.3
    # Using the following command to trigger the build
    # docker-compose -f deployments/docker-compose.yml up --build
    # build:
    #   context: ..
    #   dockerfile: ./deployments/Dockerfile
    #   args:
    #     RUNTIME: hackmdio/runtime:16.20.2-35fe7e39
    #     BUILDPACK: hackmdio/buildpack:16.20.2-35fe7e39
    environment:
      - CMD_DB_URL=postgres://codimd:change_password@database/codimd
      - CMD_USECDN=false
    depends_on:
      - database
    ports:
      - "3000:3000"
    volumes:
      - upload-data:/home/hackmd/app/public/uploads
    restart: always
volumes:
  database-data: {}
  upload-data: {}


================================================
FILE: deployments/docker-entrypoint.sh
================================================
#!/usr/bin/env bash

set -euo pipefail

if [[ "$#" -gt 0 ]]; then
    exec "$@"
    exit $?
fi

# check database and redis is ready
pcheck -env CMD_DB_URL

# run DB migrate
NEED_MIGRATE=${CMD_AUTO_MIGRATE:=true}

if [[ "$NEED_MIGRATE" = "true" ]] && [[ -f .sequelizerc ]] ; then
    npx sequelize db:migrate
fi

# start application
node app.js


================================================
FILE: lib/auth/bitbucket/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')

const bitbucketAuth = module.exports = Router()

passport.use(new BitbucketStrategy({
  clientID: config.bitbucket.clientID,
  clientSecret: config.bitbucket.clientSecret,
  callbackURL: config.serverURL + '/auth/bitbucket/callback',
  state: true
}, passportGeneralCallback))

bitbucketAuth.get('/auth/bitbucket', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('bitbucket')(req, res, next)
})

// bitbucket auth callback
bitbucketAuth.get('/auth/bitbucket/callback',
  passport.authenticate('bitbucket', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/dropbox/index.js
================================================
'use strict'

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 dropboxAuth = module.exports = Router()

passport.use(new DropboxStrategy({
  apiVersion: '2',
  clientID: config.dropbox.clientID,
  clientSecret: config.dropbox.clientSecret,
  callbackURL: config.serverURL + '/auth/dropbox/callback',
  state: true
}, passportGeneralCallback))

dropboxAuth.get('/auth/dropbox', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('dropbox-oauth2')(req, res, next)
})

// dropbox auth callback
dropboxAuth.get('/auth/dropbox/callback',
  passport.authenticate('dropbox-oauth2', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/email/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
const sequelize = require('sequelize')
const validator = require('validator')
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 response = require('../../response')

const emailAuth = module.exports = Router()

passport.use(new LocalStrategy({
  usernameField: 'email'
}, async function (email, password, done) {
  if (!validator.isEmail(email)) return done(null, false)

  try {
    const user = await models.User.findOne({
      where: {
        email: sequelize.where(
          sequelize.fn('LOWER', sequelize.col('email')),
          email.toLowerCase()
        )
      }
    })

    if (!user) return done(null, false)
    if (!await user.verifyPassword(password)) return done(null, false)
    return done(null, user)
  } catch (err) {
    logger.error(err)
    return done(err)
  }
}))

if (config.allowEmailRegister) {
  emailAuth.post('/register', urlencodedParser, async function (req, res, next) {
    if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res)
    if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res)
    try {
      const [user, created] = await models.User.findOrCreate({
        where: {
          email: req.body.email
        },
        defaults: {
          password: req.body.password
        }
      })

      if (!user) {
        req.flash('error', 'Failed to register your account, please try again.')
        return res.redirect(config.serverURL + '/')
      }

      if (created) {
        logger.debug('user registered: ' + user.id)
        req.flash('info', "You've successfully registered, please signin.")
      } else {
        logger.debug('user found: ' + user.id)
        req.flash('error', 'This email has been used, please try another one.')
      }
      return res.redirect(config.serverURL + '/')
    } catch (err) {
      logger.error('auth callback failed: ' + err)
      return response.errorInternalError(req, res)
    }
  })
}

emailAuth.post('/login', urlencodedParser, function (req, res, next) {
  if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res)
  if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res)
  setReturnToFromReferer(req)
  passport.authenticate('local', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/',
    failureFlash: 'Invalid email or password.'
  })(req, res, next)
})


================================================
FILE: lib/auth/facebook/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
const FacebookStrategy = require('passport-facebook').Strategy

const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')

const facebookAuth = module.exports = Router()

passport.use(new FacebookStrategy({
  clientID: config.facebook.clientID,
  clientSecret: config.facebook.clientSecret,
  callbackURL: config.serverURL + '/auth/facebook/callback',
  state: true
}, passportGeneralCallback))

facebookAuth.get('/auth/facebook', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('facebook')(req, res, next)
})

// facebook auth callback
facebookAuth.get('/auth/facebook/callback',
  passport.authenticate('facebook', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/github/index.js
================================================
'use strict'

const Router = require('express').Router
const request = require('request')
const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const { InternalOAuthError } = require('passport-oauth2')
const config = require('../../config')
const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { URL } = require('url')
const { promisify } = require('util')

const rp = promisify(request)

const githubAuth = module.exports = Router()

function githubUrl (path) {
  return config.github.enterpriseURL && new URL(path, config.github.enterpriseURL).toString()
}

passport.use(new GithubStrategy({
  scope: (config.github.organizations ? config.github.scopes.concat(['read:org']) : config.github.scope),
  clientID: config.github.clientID,
  clientSecret: config.github.clientSecret,
  callbackURL: config.serverURL + '/auth/github/callback',
  authorizationURL: githubUrl('login/oauth/authorize'),
  tokenURL: githubUrl('login/oauth/access_token'),
  userProfileURL: githubUrl('api/v3/user'),
  state: true
}, async (accessToken, refreshToken, profile, done) => {
  if (!config.github.organizations) {
    return passportGeneralCallback(accessToken, refreshToken, profile, done)
  }
  const { statusCode, body: data } = await rp({
    url: `https://api.github.com/user/orgs`,
    method: 'GET',
    json: true,
    timeout: 2000,
    headers: {
      Authorization: `token ${accessToken}`,
      'User-Agent': 'nodejs-http'
    }
  })
  if (statusCode !== 200) {
    return done(InternalOAuthError(
      `Failed to query organizations for user: ${profile.username}`
    ))
  }
  const orgs = data.map(({ login }) => login)
  for (const org of orgs) {
    if (config.github.organizations.includes(org)) {
      return passportGeneralCallback(accessToken, refreshToken, profile, done)
    }
  }
  return done(InternalOAuthError(
    `User orgs not whitelisted: ${profile.username} (${orgs.join(',')})`
  ))
}))

githubAuth.get('/auth/github', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('github')(req, res, next)
})

githubAuth.get('/auth/github/callback',
  passport.authenticate('github', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

// github callback actions
githubAuth.get('/auth/github/callback/:noteId/:action', response.githubActions)


================================================
FILE: lib/auth/gitlab/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../config')
const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const HttpsProxyAgent = require('https-proxy-agent')

const gitlabAuth = module.exports = Router()

const gitlabAuthStrategy = new GitlabStrategy({
  baseURL: config.gitlab.baseURL,
  clientID: config.gitlab.clientID,
  clientSecret: config.gitlab.clientSecret,
  scope: config.gitlab.scope,
  callbackURL: config.serverURL + '/auth/gitlab/callback',
  state: true
}, passportGeneralCallback)

if (process.env.https_proxy) {
  const httpsProxyAgent = new HttpsProxyAgent(process.env.https_proxy)
  gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent)
}

passport.use(gitlabAuthStrategy)

gitlabAuth.get('/auth/gitlab', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('gitlab')(req, res, next)
})

// gitlab auth callback
gitlabAuth.get('/auth/gitlab/callback',
  passport.authenticate('gitlab', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

if (!config.gitlab.scope || config.gitlab.scope === 'api') {
  // gitlab callback actions
  gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions)
}


================================================
FILE: lib/auth/google/index.js
================================================
'use strict'

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 googleAuth = module.exports = Router()

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',
  state: true
}, passportGeneralCallback))

googleAuth.get('/auth/google', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('google', {
    scope: ['profile'],
    hostedDomain: config.google.hostedDomain
  })(req, res, next)
})
// google auth callback
googleAuth.get('/auth/google/callback',
  passport.authenticate('google', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')

const config = require('../config')
const logger = require('../logger')
const models = require('../models')

const authRouter = module.exports = Router()

// serialize and deserialize
passport.serializeUser(function (user, done) {
  logger.info('serializeUser: ' + user.id)
  return done(null, user.id)
})

passport.deserializeUser(function (id, done) {
  models.User.findOne({
    where: {
      id: id
    }
  }).then(function (user) {
    // Don't die on non-existent user
    if (user == null) {
      return done(null, false, { message: 'Invalid UserID' })
    }

    logger.info('deserializeUser: ' + user.id)
    return done(null, user)
  }).catch(function (err) {
    logger.error(err)
    return done(err, null)
  })
})

if (config.isFacebookEnable) authRouter.use(require('./facebook'))
if (config.isTwitterEnable) authRouter.use(require('./twitter'))
if (config.isGitHubEnable) authRouter.use(require('./github'))
if (config.isBitbucketEnable) authRouter.use(require('./bitbucket'))
if (config.isGitLabEnable) authRouter.use(require('./gitlab'))
if (config.isMattermostEnable) authRouter.use(require('./mattermost'))
if (config.isDropboxEnable) authRouter.use(require('./dropbox'))
if (config.isGoogleEnable) authRouter.use(require('./google'))
if (config.isLDAPEnable) authRouter.use(require('./ldap'))
if (config.isSAMLEnable) authRouter.use(require('./saml'))
if (config.isOAuth2Enable) authRouter.use(require('./oauth2'))
if (config.isEmailEnable) authRouter.use(require('./email'))
if (config.isOpenIDEnable) authRouter.use(require('./openid'))

// logout
authRouter.get('/logout', function (req, res, next) {
  if (config.debug && req.isAuthenticated()) {
    logger.debug('user logout: ' + req.user.id)
  }

  req.logout((err) => {
    if (err) { return next(err) }

    res.redirect(config.serverURL + '/')
  })
})


================================================
FILE: lib/auth/ldap/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
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 response = require('../../response')

const ldapAuth = module.exports = Router()

passport.use(new LDAPStrategy({
  server: {
    url: config.ldap.url || null,
    bindDN: config.ldap.bindDn || null,
    bindCredentials: config.ldap.bindCredentials || null,
    searchBase: config.ldap.searchBase || null,
    searchFilter: config.ldap.searchFilter || null,
    searchAttributes: config.ldap.searchAttributes || null,
    tlsOptions: config.ldap.tlsOptions || null
  }
}, function (user, done) {
  var uuid = user.uidNumber || user.uid || user.sAMAccountName || undefined
  if (config.ldap.useridField && user[config.ldap.useridField]) {
    uuid = user[config.ldap.useridField]
  }

  if (typeof uuid === 'undefined') {
    throw new Error('Could not determine UUID for LDAP user. Check that ' +
    'either uidNumber, uid or sAMAccountName is set in your LDAP directory ' +
    'or use another unique attribute and configure it using the ' +
    '"useridField" option in ldap settings.')
  }

  var username = uuid
  if (config.ldap.usernameField && user[config.ldap.usernameField]) {
    username = user[config.ldap.usernameField]
  }

  var profile = {
    id: 'LDAP-' + uuid,
    username: username,
    displayName: user.displayName,
    emails: user.mail ? Array.isArray(user.mail) ? user.mail : [user.mail] : [],
    avatarUrl: null,
    profileUrl: null,
    provider: 'ldap'
  }
  var stringifiedProfile = JSON.stringify(profile)
  models.User.findOrCreate({
    where: {
      profileid: profile.id.toString()
    },
    defaults: {
      profile: stringifiedProfile
    }
  }).spread(function (user, created) {
    if (user) {
      var needSave = false
      if (user.profile !== stringifiedProfile) {
        user.profile = stringifiedProfile
        needSave = true
      }
      if (needSave) {
        user.save().then(function () {
          if (config.debug) { logger.debug('user login: ' + user.id) }
          return done(null, user)
        })
      } else {
        if (config.debug) { logger.debug('user login: ' + user.id) }
        return done(null, user)
      }
    }
  }).catch(function (err) {
    logger.error('ldap auth failed: ' + err)
    return done(err, null)
  })
}))

ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
  if (!req.body.username || !req.body.password) return response.errorBadRequest(req, res)
  setReturnToFromReferer(req)
  passport.authenticate('ldapauth', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/',
    failureFlash: true
  })(req, res, next)
})


================================================
FILE: lib/auth/mattermost/index.js
================================================
'use strict'
require('babel-polyfill')
require('isomorphic-fetch')
const Router = require('express').Router
const passport = require('passport')
const { Client4: MattermostClient } = require('@mattermost/client')
const OAuthStrategy = require('passport-oauth2').Strategy
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')

const mattermostAuth = module.exports = Router()

const mattermostClient = new MattermostClient()

const mattermostStrategy = new OAuthStrategy({
  authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
  tokenURL: config.mattermost.baseURL + '/oauth/access_token',
  clientID: config.mattermost.clientID,
  clientSecret: config.mattermost.clientSecret,
  callbackURL: config.serverURL + '/auth/mattermost/callback'
}, passportGeneralCallback)

mattermostStrategy.userProfile = (accessToken, done) => {
  mattermostClient.setUrl(config.mattermost.baseURL)
  mattermostClient.setToken(accessToken)
  mattermostClient.getMe()
    .then((data) => done(null, data))
    .catch((err) => done(err))
}

passport.use(mattermostStrategy)

mattermostAuth.get('/auth/mattermost', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('oauth2')(req, res, next)
})

// mattermost auth callback
mattermostAuth.get('/auth/mattermost/callback',
  passport.authenticate('oauth2', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/oauth2/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')

const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { OAuth2CustomStrategy } = require('./strategy')

const oauth2Auth = module.exports = Router()

passport.use(new OAuth2CustomStrategy({
  authorizationURL: config.oauth2.authorizationURL,
  tokenURL: config.oauth2.tokenURL,
  clientID: config.oauth2.clientID,
  clientSecret: config.oauth2.clientSecret,
  callbackURL: config.serverURL + '/auth/oauth2/callback',
  userProfileURL: config.oauth2.userProfileURL,
  state: config.oauth2.state,
  scope: config.oauth2.scope
}, passportGeneralCallback))

oauth2Auth.get('/auth/oauth2', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('oauth2')(req, res, next)
})

// github auth callback
oauth2Auth.get('/auth/oauth2/callback',
  passport.authenticate('oauth2', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/oauth2/strategy.js
================================================
'use strict'

const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../config')

function parseProfile (data) {
  const id = extractProfileAttribute(data, config.oauth2.userProfileIdAttr)
  const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
  const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
  const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
  const photo = extractProfileAttribute(data, config.oauth2.userProfilePhotoAttr)

  if (!username) {
    throw new Error('cannot fetch username: please set correct CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR')
  }

  return {
    id: id || username,
    username: username,
    displayName: displayName,
    email: email,
    photo: photo
  }
}

function extractProfileAttribute (data, path) {
  if (!data) return undefined
  if (typeof path !== 'string') return undefined
  // can handle stuff like `attrs[0].name`
  path = path.split('.')
  for (const segment of path) {
    const m = segment.match(/([\d\w]+)\[(.*)\]/)
    if (!m) {
      data = data[segment]
    } else {
      if (m.length < 3) return undefined
      if (!data[m[1]]) return undefined
      data = data[m[1]][m[2]]
    }
    if (!data) return undefined
  }
  return data
}

function checkAuthorization (data, done) {
  const roles = extractProfileAttribute(data, config.oauth2.rolesClaim)

  if (config.oauth2.accessRole && roles) {
    if (!roles.includes(config.oauth2.accessRole)) {
      return done('Permission denied', null)
    }
  }
}

class OAuth2CustomStrategy extends Strategy {
  constructor (options, verify) {
    options.customHeaders = options.customHeaders || {}
    super(options, verify)
    this.name = 'oauth2'
    this._userProfileURL = options.userProfileURL
    this._oauth2.useAuthorizationHeaderforGET(true)
  }

  userProfile (accessToken, done) {
    this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
      if (err) {
        return done(new InternalOAuthError('Failed to fetch user profile', err))
      }

      let profile, json
      try {
        json = JSON.parse(body)
        checkAuthorization(json, done)
        profile = parseProfile(json)
      } catch (ex) {
        return done(new InternalOAuthError('Failed to parse user profile' + ex.toString()))
      }

      profile.provider = 'oauth2'

      done(null, profile)
    })
  }
}

exports.OAuth2CustomStrategy = OAuth2CustomStrategy
exports.parseProfile = parseProfile
exports.extractProfileAttribute = extractProfileAttribute


================================================
FILE: lib/auth/openid/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
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 openIDAuth = module.exports = Router()

passport.use(new OpenIDStrategy({
  returnURL: config.serverURL + '/auth/openid/callback',
  realm: config.serverURL,
  profile: true
}, function (openid, profile, done) {
  var stringifiedProfile = JSON.stringify(profile)
  models.User.findOrCreate({
    where: {
      profileid: openid
    },
    defaults: {
      profile: stringifiedProfile
    }
  }).spread(function (user, created) {
    if (user) {
      var needSave = false
      if (user.profile !== stringifiedProfile) {
        user.profile = stringifiedProfile
        needSave = true
      }
      if (needSave) {
        user.save().then(function () {
          if (config.debug) { logger.info('user login: ' + user.id) }
          return done(null, user)
        })
      } else {
        if (config.debug) { logger.info('user login: ' + user.id) }
        return done(null, user)
      }
    }
  }).catch(function (err) {
    logger.error('auth callback failed: ' + err)
    return done(err, null)
  })
}))

openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('openid')(req, res, next)
})

// openID auth callback
openIDAuth.get('/auth/openid/callback',
  passport.authenticate('openid', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/saml/index.js
================================================
'use strict'

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

const samlAuth = module.exports = Router()

passport.use(new SamlStrategy({
  callbackUrl: config.serverURL + '/auth/saml/callback',
  entryPoint: config.saml.idpSsoUrl,
  issuer: config.saml.issuer || config.serverURL,
  cert: fs.readFileSync(config.saml.idpCert, 'utf-8'),
  identifierFormat: config.saml.identifierFormat,
  disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext
}, function (user, done) {
  // check authorization if needed
  if (config.saml.externalGroups && config.saml.groupAttribute) {
    var externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
    if (externalGroups.length > 0) {
      logger.error('saml permission denied: ' + externalGroups.join(', '))
      return done('Permission denied', null)
    }
  }
  if (config.saml.requiredGroups && config.saml.groupAttribute) {
    if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) {
      logger.error('saml permission denied')
      return done('Permission denied', null)
    }
  }
  // user creation
  var uuid = user[config.saml.attribute.id] || user.nameID
  var profile = {
    provider: 'saml',
    id: 'SAML-' + uuid,
    username: user[config.saml.attribute.username] || user.nameID,
    emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : []
  }
  if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') {
    profile.emails.push(user.nameID)
  }
  var stringifiedProfile = JSON.stringify(profile)
  models.User.findOrCreate({
    where: {
      profileid: profile.id.toString()
    },
    defaults: {
      profile: stringifiedProfile
    }
  }).spread(function (user, created) {
    if (user) {
      var needSave = false
      if (user.profile !== stringifiedProfile) {
        user.profile = stringifiedProfile
        needSave = true
      }
      if (needSave) {
        user.save().then(function () {
          if (config.debug) { logger.debug('user login: ' + user.id) }
          return done(null, user)
        })
      } else {
        if (config.debug) { logger.debug('user login: ' + user.id) }
        return done(null, user)
      }
    }
  }).catch(function (err) {
    logger.error('saml auth failed: ' + err)
    return done(err, null)
  })
}))

samlAuth.get('/auth/saml',
  passport.authenticate('saml', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

samlAuth.post('/auth/saml/callback', urlencodedParser,
  passport.authenticate('saml', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

samlAuth.get('/auth/saml/metadata', function (req, res) {
  res.type('application/xml')
  res.send(passport._strategy('saml').generateServiceProviderMetadata())
})


================================================
FILE: lib/auth/twitter/index.js
================================================
'use strict'

const Router = require('express').Router
const passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy

const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')

const twitterAuth = module.exports = Router()

passport.use(new TwitterStrategy({
  consumerKey: config.twitter.consumerKey,
  consumerSecret: config.twitter.consumerSecret,
  callbackURL: config.serverURL + '/auth/twitter/callback',
  state: true
}, passportGeneralCallback))

twitterAuth.get('/auth/twitter', function (req, res, next) {
  setReturnToFromReferer(req)
  passport.authenticate('twitter')(req, res, next)
})

// twitter auth callback
twitterAuth.get('/auth/twitter/callback',
  passport.authenticate('twitter', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)


================================================
FILE: lib/auth/utils.js
================================================
'use strict'

const models = require('../models')
const config = require('../config')
const logger = require('../logger')

exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
  if (!req.session) req.session = {}

  var referer = req.get('referer')
  var nextURL
  if (referer) {
    try {
      var refererSearchParams = new URLSearchParams(new URL(referer).search)
      nextURL = refererSearchParams.get('next')
    } catch (err) {
      logger.warn(err)
    }
  }

  if (nextURL) {
    var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//')
    if (isRelativeNextURL) {
      req.session.returnTo = (new URL(nextURL, config.serverURL)).toString()
    } else {
      req.session.returnTo = config.serverURL
    }
  } else {
    req.session.returnTo = referer
  }
}

exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {
  var stringifiedProfile = JSON.stringify(profile)
  models.User.findOrCreate({
    where: {
      profileid: profile.id.toString()
    },
    defaults: {
      profile: stringifiedProfile,
      accessToken: accessToken,
      refreshToken: refreshToken
    }
  }).spread(function (user, created) {
    if (user) {
      var needSave = false
      if (user.profile !== stringifiedProfile) {
        user.profile = stringifiedProfile
        needSave = true
      }
      if (user.accessToken !== accessToken) {
        user.accessToken = accessToken
        needSave = true
      }
      if (user.refreshToken !== refreshToken) {
        user.refreshToken = refreshToken
        needSave = true
      }
      if (needSave) {
        user.save().then(function () {
          if (config.debug) { logger.info('user login: ' + user.id) }
          return done(null, user)
        })
      } else {
        if (config.debug) { logger.info('user login: ' + user.id) }
        return done(null, user)
      }
    }
  }).catch(function (err) {
    logger.error('auth callback failed: ' + err)
    return done(err, null)
  })
}


================================================
FILE: lib/config/default.js
================================================
'use strict'

const os = require('os')

module.exports = {
  domain: '',
  urlPath: '',
  host: '0.0.0.0',
  port: 3000,
  loglevel: 'info',
  urlAddPort: false,
  allowOrigin: ['localhost'],
  useSSL: false,
  hsts: {
    enable: true,
    maxAgeSeconds: 60 * 60 * 24 * 365,
    includeSubdomains: false,
    preload: true
  },
  csp: {
    enable: true,
    directives: {
    },
    addDefaults: true,
    addDisqus: true,
    addGoogleAnalytics: true,
    upgradeInsecureRequests: 'auto',
    reportURI: undefined
  },
  protocolUseSSL: false,
  useCDN: true,
  allowAnonymous: false,
  allowAnonymousEdits: true,
  allowAnonymousViews: true,
  allowFreeURL: false,
  forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api'],
  defaultPermission: 'editable',
  dbURL: '',
  db: {},
  privacyPolicyURL: '',
  // ssl path
  sslKeyPath: '',
  sslCertPath: '',
  sslCAPath: '',
  dhParamPath: '',
  // other path
  viewPath: './public/views',
  tmpPath: os.tmpdir(),
  defaultNotePath: './public/default.md',
  docsPath: './public/docs',
  uploadsPath: './public/uploads',
  // session
  sessionName: 'connect.sid',
  sessionSecret: 'secret',
  sessionSecretLen: 128,
  sessionLife: 14 * 24 * 60 * 60 * 1000, // 14 days
  staticCacheTime: 1 * 24 * 60 * 60 * 1000, // 1 day
  // socket.io
  heartbeatInterval: 5000,
  heartbeatTimeout: 10000,
  // toobusy-js
  responseMaxLag: 70,
  // document
  documentMaxLength: 100000,
  // image upload setting, available options are imgur/s3/filesystem/azure/lutim
  imageUploadType: 'filesystem',
  lutim: {
    url: 'https://framapic.org/'
  },
  imgur: {
    clientID: undefined
  },
  s3: {
    accessKeyId: undefined,
    secretAccessKey: undefined,
    region: undefined,
    endpoint: undefined,
    baseURL: undefined
  },
  minio: {
    accessKey: undefined,
    secretKey: undefined,
    endPoint: undefined,
    secure: true,
    port: 9000
  },
  s3bucket: undefined,
  azure: {
    connectionString: undefined,
    container: undefined
  },
  // authentication
  oauth2: {
    providerName: undefined,
    authorizationURL: undefined,
    tokenURL: undefined,
    clientID: undefined,
    clientSecret: undefined,
    baseURL: undefined,
    userProfileURL: undefined,
    userProfileUsernameAttr: 'username',
    userProfileDisplayNameAttr: 'displayName',
    userProfileEmailAttr: 'email',
    userProfilePhotoAttr: 'photo',
    state: true,
    scope: 'email'
  },
  facebook: {
    clientID: undefined,
    clientSecret: undefined
  },
  twitter: {
    consumerKey: undefined,
    consumerSecret: undefined
  },
  github: {
    enterpriseURL: undefined, // if you use github.com, not need to specify
    clientID: undefined,
    clientSecret: undefined,
    organizations: [],
    scopes: ['read:user']
  },
  gitlab: {
    baseURL: undefined,
    clientID: undefined,
    clientSecret: undefined,
    scope: undefined,
    version: 'v4'
  },
  mattermost: {
    baseURL: undefined,
    clientID: undefined,
    clientSecret: undefined
  },
  dropbox: {
    clientID: undefined,
    clientSecret: undefined,
    appKey: undefined
  },
  google: {
    clientID: undefined,
    clientSecret: undefined,
    hostedDomain: undefined
  },
  ldap: {
    providerName: undefined,
    url: undefined,
    bindDn: undefined,
    bindCredentials: undefined,
    searchBase: undefined,
    searchFilter: undefined,
    searchAttributes: undefined,
    usernameField: undefined,
    useridField: undefined,
    tlsca: undefined
  },
  saml: {
    idpSsoUrl: undefined,
    idpCert: undefined,
    issuer: undefined,
    identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
    disableRequestedAuthnContext: false,
    groupAttribute: undefined,
    externalGroups: [],
    requiredGroups: [],
    attribute: {
      id: undefined,
      username: undefined,
      email: undefined
    }
  },
  plantuml: {
    server: 'https://www.plantuml.com/plantuml'
  },
  email: true,
  allowEmailRegister: true,
  allowGravatar: true,
  allowPDFExport: true,
  openID: false,
  defaultUseHardbreak: true,
  // linkifyHeaderStyle - How is a header text converted into a link id.
  // Header Example: "3.1. Good Morning my Friend! - Do you have 5$?"
  // * 'keep-case' is the legacy CodiMD value.
  //    Generated id: "31-Good-Morning-my-Friend---Do-you-have-5"
  // * 'lower-case' is the same like legacy (see above), but converted to lower-case.
  //    Generated id: "#31-good-morning-my-friend---do-you-have-5"
  // * 'gfm' _GitHub-Flavored Markdown_ style as described here:
  //    https://gist.github.com/asabaylus/3071099#gistcomment-1593627
  //    It works like 'lower-case', but making sure the ID is unique.
  //    This is What GitHub, GitLab and (hopefully) most other tools use.
  //    Generated id:   "31-good-morning-my-friend---do-you-have-5"
  //    2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
  //    3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
  linkifyHeaderStyle: 'keep-case',
  autoVersionCheck: true,
  defaultTocDepth: 3
}


================================================
FILE: lib/config/defaultSSL.js
================================================
'use strict'

const fs = require('fs')

function getFile (path) {
  if (fs.existsSync(path)) {
    return path
  }
  return undefined
}

module.exports = {
  sslKeyPath: getFile('/run/secrets/key.pem'),
  sslCertPath: getFile('/run/secrets/cert.pem'),
  sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [],
  dhParamPath: getFile('/run/secrets/dhparam.pem')
}


================================================
FILE: lib/config/dockerSecret.js
================================================
'use strict'

const fs = require('fs')
const path = require('path')

const basePath = path.resolve('/var/run/secrets/')

function getSecret (secret) {
  const filePath = path.join(basePath, secret)
  if (fs.existsSync(filePath)) return fs.readFileSync(filePath)
  return undefined
}

if (fs.existsSync(basePath)) {
  module.exports = {
    dbURL: getSecret('dburl'),
    // ssl path
    sslKeyPath: getSecret('sslkeypath'),
    sslCertPath: getSecret('sslcertpath'),
    sslCAPath: getSecret('sslcapath'),
    dhParamPath: getSecret('dhparampath'),
    // session
    sessionSecret: getSecret('sessionsecret'),
    imgur: {
      clientID: getSecret('imgur_clientid')
    },
    s3: {
      accessKeyId: getSecret('s3_acccessKeyId'),
      secretAccessKey: getSecret('s3_secretAccessKey')
    },
    minio: {
      accessKey: getSecret('minio_accessKey'),
      secretKey: getSecret('minio_secretKey')
    },
    azure: {
      connectionString: getSecret('azure_connectionString')
    },
    oauth2: {
      clientID: getSecret('oauth2_clientID'),
      clientSecret: getSecret('oauth2_clientSecret')
    },
    facebook: {
      clientID: getSecret('facebook_clientID'),
      clientSecret: getSecret('facebook_clientSecret')
    },
    twitter: {
      consumerKey: getSecret('twitter_consumerKey'),
      consumerSecret: getSecret('twitter_consumerSecret')
    },
    github: {
      clientID: getSecret('github_clientID'),
      clientSecret: getSecret('github_clientSecret')
    },
    gitlab: {
      clientID: getSecret('gitlab_clientID'),
      clientSecret: getSecret('gitlab_clientSecret')
    },
    mattermost: {
      clientID: getSecret('mattermost_clientID'),
      clientSecret: getSecret('mattermost_clientSecret')
    },
    dropbox: {
      clientID: getSecret('dropbox_clientID'),
      clientSecret: getSecret('dropbox_clientSecret'),
      appKey: getSecret('dropbox_appKey')
    },
    google: {
      clientID: getSecret('google_clientID'),
      clientSecret: getSecret('google_clientSecret')
    },
    ldap: {
      bindCredentials: getSecret('ldap_bindCredentials'),
      tlsca: getSecret('ldap_tlsca')
    },
    saml: {
      idpCert: getSecret('saml_idpCert')
    }
  }
}


================================================
FILE: lib/config/enum.js
================================================
'use strict'

exports.Environment = {
  development: 'development',
  production: 'production',
  test: 'test'
}

exports.Permission = {
  freely: 'freely',
  editable: 'editable',
  limited: 'limited',
  locked: 'locked',
  protected: 'protected',
  private: 'private'
}


================================================
FILE: lib/config/environment.js
================================================
'use strict'

const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')

module.exports = {
  sourceURL: process.env.CMD_SOURCE_URL,
  domain: process.env.CMD_DOMAIN,
  urlPath: process.env.CMD_URL_PATH,
  host: process.env.CMD_HOST,
  port: toIntegerConfig(process.env.CMD_PORT),
  path: process.env.CMD_PATH,
  loglevel: process.env.CMD_LOGLEVEL,
  urlAddPort: toBooleanConfig(process.env.CMD_URL_ADDPORT),
  useSSL: toBooleanConfig(process.env.CMD_USESSL),
  hsts: {
    enable: toBooleanConfig(process.env.CMD_HSTS_ENABLE),
    maxAgeSeconds: toIntegerConfig(process.env.CMD_HSTS_MAX_AGE),
    includeSubdomains: toBooleanConfig(process.env.CMD_HSTS_INCLUDE_SUBDOMAINS),
    preload: toBooleanConfig(process.env.CMD_HSTS_PRELOAD)
  },
  csp: {
    enable: toBooleanConfig(process.env.CMD_CSP_ENABLE),
    reportURI: process.env.CMD_CSP_REPORTURI
  },
  protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL),
  allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN),
  useCDN: toBooleanConfig(process.env.CMD_USECDN),
  allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS),
  allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS),
  allowAnonymousViews: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_VIEWS),
  allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
  forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
  defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
  dbURL: process.env.CMD_DB_URL,
  sessionSecret: process.env.CMD_SESSION_SECRET,
  sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
  responseMaxLag: toIntegerConfig(process.env.CMD_RESPONSE_MAX_LAG),
  privacyPolicyURL: process.env.CMD_PRIVACY_POLICY_URL,
  imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
  imgur: {
    clientID: process.env.CMD_IMGUR_CLIENTID
  },
  s3: {
    accessKeyId: process.env.CMD_S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.CMD_S3_SECRET_ACCESS_KEY,
    region: process.env.CMD_S3_REGION,
    endpoint: process.env.CMD_S3_ENDPOINT,
    baseURL: process.env.CMD_S3_BASEURL
  },
  minio: {
    accessKey: process.env.CMD_MINIO_ACCESS_KEY,
    secretKey: process.env.CMD_MINIO_SECRET_KEY,
    endPoint: process.env.CMD_MINIO_ENDPOINT,
    secure: toBooleanConfig(process.env.CMD_MINIO_SECURE),
    port: toIntegerConfig(process.env.CMD_MINIO_PORT)
  },
  s3bucket: process.env.CMD_S3_BUCKET,
  azure: {
    connectionString: process.env.CMD_AZURE_CONNECTION_STRING,
    container: process.env.CMD_AZURE_CONTAINER
  },
  facebook: {
    clientID: process.env.CMD_FACEBOOK_CLIENTID,
    clientSecret: process.env.CMD_FACEBOOK_CLIENTSECRET
  },
  twitter: {
    consumerKey: process.env.CMD_TWITTER_CONSUMERKEY,
    consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET
  },
  github: {
    enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
    clientID: process.env.CMD_GITHUB_CLIENTID,
    clientSecret: process.env.CMD_GITHUB_CLIENTSECRET,
    organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS),
    scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES)
  },
  bitbucket: {
    clientID: process.env.CMD_BITBUCKET_CLIENTID,
    clientSecret: process.env.CMD_BITBUCKET_CLIENTSECRET
  },
  gitlab: {
    baseURL: process.env.CMD_GITLAB_BASEURL,
    clientID: process.env.CMD_GITLAB_CLIENTID,
    clientSecret: process.env.CMD_GITLAB_CLIENTSECRET,
    scope: process.env.CMD_GITLAB_SCOPE
  },
  mattermost: {
    baseURL: process.env.CMD_MATTERMOST_BASEURL,
    clientID: process.env.CMD_MATTERMOST_CLIENTID,
    clientSecret: process.env.CMD_MATTERMOST_CLIENTSECRET
  },
  oauth2: {
    providerName: process.env.CMD_OAUTH2_PROVIDERNAME,
    baseURL: process.env.CMD_OAUTH2_BASEURL,
    clientID: process.env.CMD_OAUTH2_CLIENT_ID,
    clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET,
    authorizationURL: process.env.CMD_OAUTH2_AUTHORIZATION_URL,
    tokenURL: process.env.CMD_OAUTH2_TOKEN_URL,
    userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL,
    scope: process.env.CMD_OAUTH2_SCOPE,
    state: process.env.CMD_OAUTH2_STATE,
    rolesClaim: process.env.CMD_OAUTH2_ROLES_CLAIM,
    accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE,
    userProfileIdAttr: process.env.CMD_OAUTH2_USER_PROFILE_ID_ATTR,
    userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
    userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
    userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,
    userProfilePhotoAttr: process.env.CMD_OAUTH2_USER_PROFILE_PHOTO_ATTR
  },
  dropbox: {
    clientID: process.env.CMD_DROPBOX_CLIENTID,
    clientSecret: process.env.CMD_DROPBOX_CLIENTSECRET,
    appKey: process.env.CMD_DROPBOX_APPKEY
  },
  google: {
    clientID: process.env.CMD_GOOGLE_CLIENTID,
    clientSecret: process.env.CMD_GOOGLE_CLIENTSECRET,
    hostedDomain: process.env.CMD_GOOGLE_HOSTEDDOMAIN
  },
  ldap: {
    providerName: process.env.CMD_LDAP_PROVIDERNAME,
    url: process.env.CMD_LDAP_URL,
    bindDn: process.env.CMD_LDAP_BINDDN,
    bindCredentials: process.env.CMD_LDAP_BINDCREDENTIALS,
    searchBase: process.env.CMD_LDAP_SEARCHBASE,
    searchFilter: process.env.CMD_LDAP_SEARCHFILTER,
    searchAttributes: toArrayConfig(process.env.CMD_LDAP_SEARCHATTRIBUTES),
    usernameField: process.env.CMD_LDAP_USERNAMEFIELD,
    useridField: process.env.CMD_LDAP_USERIDFIELD,
    tlsca: process.env.CMD_LDAP_TLS_CA
  },
  saml: {
    idpSsoUrl: process.env.CMD_SAML_IDPSSOURL,
    idpCert: process.env.CMD_SAML_IDPCERT,
    issuer: process.env.CMD_SAML_ISSUER,
    identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT,
    disableRequestedAuthnContext: toBooleanConfig(process.env.CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT),
    groupAttribute: process.env.CMD_SAML_GROUPATTRIBUTE,
    externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []),
    requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []),
    attribute: {
      id: process.env.CMD_SAML_ATTRIBUTE_ID,
      username: process.env.CMD_SAML_ATTRIBUTE_USERNAME,
      email: process.env.CMD_SAML_ATTRIBUTE_EMAIL
    }
  },
  plantuml: {
    server: process.env.CMD_PLANTUML_SERVER
  },
  email: toBooleanConfig(process.env.CMD_EMAIL),
  allowEmailRegister: toBooleanConfig(process.env.CMD_ALLOW_EMAIL_REGISTER),
  allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR),
  allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT),
  openID: toBooleanConfig(process.env.CMD_OPENID),
  defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
  linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
  autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK),
  defaultTocDepth: toIntegerConfig(process.env.CMD_DEFAULT_TOC_DEPTH)
}


================================================
FILE: lib/config/index.js
================================================

'use strict'

const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const { merge } = require('lodash')
const deepFreeze = require('deep-freeze')
const { Environment, Permission } = require('./enum')
const logger = require('../logger')
const { getGitCommit, getGitHubURL } = require('./utils')

const appRootPath = path.resolve(__dirname, '../../')
const env = process.env.NODE_ENV || Environment.development
const debugConfig = {
  debug: (env === Environment.development)
}

// Get version string from package.json
const { version, repository } = require(path.join(appRootPath, 'package.json'))

const commitID = getGitCommit(appRootPath)
const sourceURL = getGitHubURL(repository.url, commitID || version)
const fullversion = commitID ? `${version}-${commitID}` : version

const packageConfig = {
  version: version,
  minimumCompatibleVersion: '0.5.0',
  fullversion: fullversion,
  sourceURL: sourceURL
}

const configFilePath = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE ||
'config.json')
const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined

let config = require('./default')
merge(config, require('./defaultSSL'))
merge(config, debugConfig)
merge(config, packageConfig)
merge(config, fileConfig)
merge(config, require('./environment'))
merge(config, require('./dockerSecret'))

if (['debug', 'verbose', 'info', 'warn', 'error'].includes(config.loglevel)) {
  logger.level = config.loglevel
} else {
  logger.error('Selected loglevel %s doesn\'t exist, using default level \'debug\'. Available options: debug, verbose, info, warn, error', config.loglevel)
}

// load LDAP CA
if (config.ldap.tlsca) {
  const certificateAuthorities = config.ldap.tlsca.split(',')
  const caContent = []
  for (const ca of certificateAuthorities) {
    if (fs.existsSync(ca)) {
      caContent.push(fs.readFileSync(ca, 'utf8'))
    }
  }
  const tlsOptions = {
    ca: caContent
  }
  config.ldap.tlsOptions = config.ldap.tlsOptions ? Object.assign(config.ldap.tlsOptions, tlsOptions) : tlsOptions
}

// Permission
config.permission = Permission
let defaultPermission = config.permission.editable
if (!config.allowAnonymous && !config.allowAnonymousViews) {
  delete config.permission.freely
  delete config.permission.editable
  delete config.permission.locked
  defaultPermission = config.permission.limited
} else if (!config.allowAnonymous && !config.allowAnonymousEdits) {
  delete config.permission.freely
}
if (!(config.defaultPermission in config.permission)) {
  config.defaultPermission = defaultPermission
}

// cache result, cannot change config in runtime!!!
config.isStandardHTTPsPort = (function isStandardHTTPsPort () {
  return config.useSSL && config.port === 443
})()
config.isStandardHTTPPort = (function isStandardHTTPPort () {
  return !config.useSSL && config.port === 80
})()

// cache serverURL
config.serverURL = (function getserverurl () {
  var url = ''
  if (config.domain) {
    var protocol = config.protocolUseSSL ? 'https://' : 'http://'
    url = protocol + config.domain
    if (config.urlAddPort) {
      if (!config.isStandardHTTPPort || !config.isStandardHTTPsPort) {
        url += ':' + config.port
      }
    }
  }
  if (config.urlPath) {
    url += '/' + config.urlPath
  }
  return url
})()

if (config.serverURL === '') {
  logger.warn('Neither \'domain\' nor \'CMD_DOMAIN\' is configured. This can cause issues with various components.\nHint: Make sure \'protocolUseSSL\' and \'urlAddPort\' or \'CMD_PROTOCOL_USESSL\' and \'CMD_URL_ADDPORT\' are configured properly.')
}

config.Environment = Environment

// auth method
config.isFacebookEnable = config.facebook.clientID && config.facebook.clientSecret
config.isGoogleEnable = config.google.clientID && config.google.clientSecret
config.isDropboxEnable = config.dropbox.clientID && config.dropbox.clientSecret
config.isTwitterEnable = config.twitter.consumerKey && config.twitter.consumerSecret
config.isEmailEnable = config.email
config.isOpenIDEnable = config.openID
config.isGitHubEnable = config.github.clientID && config.github.clientSecret
config.isBitbucketEnable = config.bitbucket.clientID && config.bitbucket.clientSecret
config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret
config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clientSecret
config.isLDAPEnable = config.ldap.url
config.isSAMLEnable = config.saml.idpSsoUrl
config.isOAuth2Enable = config.oauth2.clientID && config.oauth2.clientSecret
config.isPDFExportEnable = config.allowPDFExport

// Check gitlab api version
if (config.gitlab && config.gitlab.version !== 'v4' && config.gitlab.version !== 'v3') {
  logger.warn('config.js contains wrong version (' + config.gitlab.version + ') for gitlab api; it should be \'v3\' or \'v4\'. Defaulting to v4')
  config.gitlab.version = 'v4'
}
// If gitlab scope is api, enable snippets Export/import
config.isGitlabSnippetsEnable = (!config.gitlab.scope || config.gitlab.scope === 'api') && config.isGitLabEnable

// Only update i18n files in development setups
config.updateI18nFiles = (env === Environment.development)

// merge legacy values
const keys = Object.keys(config)
const uppercase = /[A-Z]/
for (let i = keys.length; i--;) {
  const lowercaseKey = keys[i].toLowerCase()
  // if the config contains uppercase letters
  // and a lowercase version of this setting exists
  // and the config with uppercase is not set
  // we set the new config using the old key.
  if (uppercase.test(keys[i]) &&
  config[lowercaseKey] !== undefined &&
  fileConfig[keys[i]] === undefined) {
    logger.warn('config.js contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.js file to replace ' + lowercaseKey + ' with ' + keys[i])
    config[keys[i]] = config[lowercaseKey]
  }
}

// Notify users about the prefix change and inform them they use legacy prefix for environment variables
if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) {
  logger.warn('Using legacy HMD prefix for environment variables. Please change your variables in future. For details see: https://hackmd.io/c/codimd-documentation/%2F%40codimd%2Fmigrate-2-0#1-Drop-old-environment-variables-support')
}

// Generate session secret if it stays on default values
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
}

// Validate upload upload providers
if (['filesystem', 's3', 'minio', 'imgur', 'azure', 'lutim'].indexOf(config.imageUploadType) === -1) {
  logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure", "lutim" or "imgur". Defaulting to "filesystem"')
  config.imageUploadType = 'filesystem'
}

// figure out mime types for image uploads
switch (config.imageUploadType) {
  case 'imgur':
    config.allowedUploadMimeTypes = [
      'image/jpeg',
      'image/png',
      'image/jpg',
      'image/gif'
    ]
    break
  default:
    config.allowedUploadMimeTypes = [
      'image/jpeg',
      'image/png',
      'image/jpg',
      'image/gif',
      'image/svg+xml',
      'image/bmp',
      'image/tiff'
    ]
}

// generate correct path
config.sslCAPath.forEach(function (capath, i, array) {
  array[i] = path.resolve(appRootPath, capath)
})

config.appRootPath = appRootPath
config.sslCertPath = path.resolve(appRootPath, config.sslCertPath)
config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath)
config.dhParamPath = path.resolve(appRootPath, config.dhParamPath)
config.viewPath = path.resolve(appRootPath, config.viewPath)
config.tmpPath = path.resolve(appRootPath, config.tmpPath)
config.defaultNotePath = path.resolve(appRootPath, config.defaultNotePath)
config.docsPath = path.resolve(appRootPath, config.docsPath)
config.uploadsPath = path.resolve(appRootPath, config.uploadsPath)

// make config readonly
config = deepFreeze(config)

module.exports = config


================================================
FILE: lib/config/utils.js
================================================
'use strict'

const fs = require('fs')
const path = require('path')

exports.toBooleanConfig = function toBooleanConfig (configValue) {
  if (configValue && typeof configValue === 'string') {
    return (configValue === 'true')
  }
  return configValue
}

exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) {
  if (configValue && typeof configValue === 'string') {
    return (configValue.split(separator).map(arrayItem => arrayItem.trim()))
  }
  return fallback
}

exports.toIntegerConfig = function toIntegerConfig (configValue) {
  if (configValue && typeof configValue === 'string') {
    return parseInt(configValue)
  }
  return configValue
}

exports.getGitCommit = function getGitCommit (repodir) {
  if (!fs.existsSync(repodir + '/.git/HEAD')) {
    return undefined
  }
  let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8')
  if (reference.startsWith('ref: ')) {
    reference = reference.substr(5).replace('\n', '')
    reference = fs.readFileSync(path.resolve(repodir + '/.git', reference), 'utf8')
  }
  reference = reference.replace('\n', '')
  return reference
}

exports.getGitHubURL = function getGitHubURL (repo, reference) {
  // if it's not a github reference, we handle handle that anyway
  if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) {
    return repo
  }
  if (repo.startsWith('git@github.com') || repo.startsWith('ssh://git@github.com')) {
    repo = repo.replace(/^(ssh:\/\/)?git@github.com:/, 'https://github.com/')
  }

  if (repo.endsWith('.git')) {
    repo = repo.replace(/\.git$/, '/')
  } else if (!repo.endsWith('/')) {
    repo = repo + '/'
  }
  return repo + 'tree/' + reference
}


================================================
FILE: lib/csp.js
================================================
var config = require('./config')
var uuid = require('uuid')

var CspStrategy = {}

var defaultDirectives = {
  defaultSrc: ['\'self\''],
  scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''],
  // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/codimd/issues/594
  imgSrc: ['*', 'data:'],
  styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views
  fontSrc: ['\'self\'', 'data:', 'https://public.slidesharecdn.com'],
  objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/
  mediaSrc: ['*'],
  childSrc: ['*'],
  connectSrc: ['*']
}

var dropboxDirectives = {
  scriptSrc: ['https://www.dropbox.com']
}

var cdnDirectives = {
  scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net', 'https://cdn.mathjax.org'],
  styleSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
  fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com']
}

var disqusDirectives = {
  scriptSrc: ['https://disqus.com', 'https://*.disqus.com', 'https://*.disquscdn.com'],
  styleSrc: ['https://*.disquscdn.com'],
  fontSrc: ['https://*.disquscdn.com']
}

var googleAnalyticsDirectives = {
  scriptSrc: ['https://www.google-analytics.com']
}

CspStrategy.computeDirectives = function () {
  var directives = {}
  mergeDirectives(directives, config.csp.directives)
  mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives)
  mergeDirectivesIf(config.useCDN, directives, cdnDirectives)
  mergeDirectivesIf(config.dropbox && config.dropbox.appKey, directives, dropboxDirectives)
  mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives)
  mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives)
  if (!areAllInlineScriptsAllowed(directives)) {
    addInlineScriptExceptions(directives)
  }
  addUpgradeUnsafeRequestsOptionTo(directives)
  addReportURI(directives)
  return directives
}

function mergeDirectives (existingDirectives, newDirectives) {
  for (var propertyName in newDirectives) {
    var newDirective = newDirectives[propertyName]
    if (newDirective) {
      var existingDirective = existingDirectives[propertyName] || []
      existingDirectives[propertyName] = existingDirective.concat(newDirective)
    }
  }
}

function mergeDirectivesIf (condition, existingDirectives, newDirectives) {
  if (condition) {
    mergeDirectives(existingDirectives, newDirectives)
  }
}

function areAllInlineScriptsAllowed (directives) {
  return directives.scriptSrc.indexOf('\'unsafe-inline\'') !== -1
}

function addInlineScriptExceptions (directives) {
  directives.scriptSrc.push(getCspNonce)
  // TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
  // Any more clean solution appreciated.
  directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
}

function getCspNonce (req, res) {
  return "'nonce-" + res.locals.nonce + "'"
}

function addUpgradeUnsafeRequestsOptionTo (directives) {
  if (config.csp.upgradeInsecureRequests === 'auto' && config.useSSL) {
    directives.upgradeInsecureRequests = true
  } else if (config.csp.upgradeInsecureRequests === true) {
    directives.upgradeInsecureRequests = true
  }
}

function addReportURI (directives) {
  if (config.csp.reportURI) {
    directives.reportUri = config.csp.reportURI
  }
}

CspStrategy.addNonceToLocals = function (req, res, next) {
  res.locals.nonce = uuid.v4()
  next()
}

module.exports = CspStrategy


================================================
FILE: lib/errorPage/index.js
================================================
'use strict'

const config = require('../config')
const { responseError } = require('../response')

exports.errorForbidden = (req, res) => {
  if (req.user) {
    return responseError(res, '403', 'Forbidden', 'oh no.')
  }

  req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
  res.redirect(config.serverURL + '/')
}

exports.errorNotFound = (req, res) => {
  responseError(res, '404', 'Not Found', 'oops.')
}

exports.errorInternalError = (req, res) => {
  responseError(res, '500', 'Internal Error', 'wtf.')
}


================================================
FILE: lib/history/index.js
================================================
'use strict'
// history
// external modules
var LZString = require('@hackmd/lz-string')

// core
var config = require('../config')
var logger = require('../logger')
var response = require('../response')
var models = require('../models')

function getHistory (userid, callback) {
  models.User.findOne({
    where: {
      id: userid
    }
  }).then(function (user) {
    if (!user) {
      return callback(null, null)
    }
    var history = {}
    if (user.history) {
      history = JSON.parse(user.history)
      // migrate LZString encoded note id to base64url encoded note id
      for (let i = 0, l = history.length; i < l; i++) {
        // 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 (!(history[i].id.length > base64UuidLength)) {
          continue
        }
        try {
          const id = LZString.decompressFromBase64(history[i].id)
          if (id && models.Note.checkNoteIdValid(id)) {
            history[i].id = models.Note.encodeNoteId(id)
          }
        } catch (err) {
          // most error here comes from LZString, ignore
          if (err.message === 'Cannot read property \'charAt\' of undefined') {
            logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.')
          } else {
            logger.error(err)
          }
        }
      }
      history = parseHistoryToObject(history)
    }
    if (config.debug) {
      logger.info('read history success: ' + user.id)
    }
    return callback(null, history)
  }).catch(function (err) {
    logger.error('read history failed: ' + err)
    return callback(err, null)
  })
}

function setHistory (userid, history, callback) {
  models.User.update({
    history: JSON.stringify(parseHistoryToArray(history))
  }, {
    where: {
      id: userid
    }
  }).then(function (count) {
    return callback(null, count)
  }).catch(function (err) {
    logger.error('set history failed: ' + err)
    return callback(err, null)
  })
}

function updateHistory (userid, noteId, document, time) {
  if (userid && noteId && typeof document !== 'undefined') {
    getHistory(userid, function (err, history) {
      if (err || !history) return
      if (!history[noteId]) {
        history[noteId] = {}
      }
      var noteHistory = history[noteId]
      var noteInfo = models.Note.parseNoteInfo(document)
      noteHistory.id = noteId
      noteHistory.text = noteInfo.title
      noteHistory.time = time || Date.now()
      noteHistory.tags = noteInfo.tags
      setHistory(userid, history, function (err, count) {
        if (err) {
          logger.log(err)
        }
      })
    })
  }
}

function parseHistoryToArray (history) {
  var _history = []
  Object.keys(history).forEach(function (key) {
    var item = history[key]
    _history.push(item)
  })
  return _history
}

function parseHistoryToObject (history) {
  var _history = {}
  for (var i = 0, l = history.length; i < l; i++) {
    var item = history[i]
    _history[item.id] = item
  }
  return _history
}

function historyGet (req, res) {
  if (req.isAuthenticated()) {
    getHistory(req.user.id, function (err, history) {
      if (err) return response.errorInternalError(req, res)
      if (!history) return response.errorNotFound(req, res)
      res.send({
        history: parseHistoryToArray(history)
      })
    })
  } else {
    return response.errorForbidden(req, res)
  }
}

function historyPost (req, res) {
  if (req.isAuthenticated()) {
    var noteId = req.params.noteId
    if (!noteId) {
      if (typeof req.body.history === 'undefined') return response.errorBadRequest(req, res)
      if (config.debug) { logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history) }
      try {
        var history = JSON.parse(req.body.history)
      } catch (err) {
        return response.errorBadRequest(req, res)
      }
      if (Array.isArray(history)) {
        setHistory(req.user.id, history, function (err, count) {
          if (err) return response.errorInternalError(req, res)
          res.end()
        })
      } else {
        return response.errorBadRequest(req, res)
      }
    } else {
      if (typeof req.body.pinned === 'undefined') return response.errorBadRequest(req, res)
      getHistory(req.user.id, function (err, history) {
        if (err) return response.errorInternalError(req, res)
        if (!history) return response.errorNotFound(req, res)
        if (!history[noteId]) return response.errorNotFound(req, res)
        if (req.body.pinned === 'true' || req.body.pinned === 'false') {
          history[noteId].pinned = (req.body.pinned === 'true')
          setHistory(req.user.id, history, function (err, count) {
            if (err) return response.errorInternalError(req, res)
            res.end()
          })
        } else {
          return response.errorBadRequest(req, res)
        }
      })
    }
  } else {
    return response.errorForbidden(req, res)
  }
}

function historyDelete (req, res) {
  if (req.isAuthenticated()) {
    var noteId = req.params.noteId
    if (!noteId) {
      setHistory(req.user.id, [], function (err, count) {
        if (err) return response.errorInternalError(req, res)
        res.end()
      })
    } else {
      getHistory(req.user.id, function (err, history) {
        if (err) return response.errorInternalError(req, res)
        if (!history) return response.errorNotFound(req, res)
        delete history[noteId]
        setHistory(req.user.id, history, function (err, count) {
          if (err) return response.errorInternalError(req, res)
          res.end()
        })
      })
    }
  } else {
    return response.errorForbidden(req, res)
  }
}

// public
exports.historyGet = historyGet
exports.historyPost = historyPost
exports.historyDelete = historyDelete
exports.updateHistory = updateHistory


================================================
FILE: lib/homepage/index.js
================================================
'use strict'

const fs = require('fs')
const path = require('path')
const config = require('../config')
const { User } = require('../models')
const logger = require('../logger')

exports.showIndex = async (req, res) => {
  const isLogin = req.isAuthenticated()
  const deleteToken = ''

  const data = {
    signin: isLogin,
    infoMessage: req.flash('info'),
    errorMessage: req.flash('error'),
    privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
    termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
    deleteToken: deleteToken,
    csrfToken: req.csrfToken()
  }

  if (!isLogin) {
    return res.render('index.ejs', data)
  }

  const user = await User.findOne({
    where: {
      id: req.user.id
    }
  })
  if (user) {
    data.deleteToken = user.deleteToken
    return res.render('index.ejs', data)
  }

  logger.error(`error: user not found with id ${req.user.id}`)
  return res.render('index.ejs', data)
}


================================================
FILE: lib/imageRouter/azure.js
================================================
'use strict'
const path = require('path')

const config = require('../config')
const logger = require('../logger')

const azure = require('azure-storage')

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  var azureBlobService = azure.createBlobService(config.azure.connectionString)

  azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) {
    if (err) {
      callback(new Error(err.message), null)
    } else {
      azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) {
        if (err) {
          callback(new Error(err.message), null)
        } else {
          callback(null, azureBlobService.getUrl(config.azure.container, result.name))
        }
      })
    }
  })
}


================================================
FILE: lib/imageRouter/filesystem.js
================================================
'use strict'

const crypto = require('crypto')
const fs = require('fs')
const URL = require('url').URL
const path = require('path')

const config = require('../config')
const logger = require('../logger')

/**
 * generate a random filename for uploaded image
 */
function randomFilename () {
  const buf = crypto.randomBytes(16)
  return `upload_${buf.toString('hex')}`
}

/**
 * pick a filename not exist in filesystem
 * maximum attempt 5 times
 */
function pickFilename (defaultFilename) {
  let retryCounter = 5
  const extname = path.extname(defaultFilename)
  let filename = `${randomFilename()}${extname}`
  while (retryCounter-- > 0) {
    if (fs.existsSync(path.join(config.uploadsPath, filename))) {
      filename = `${randomFilename()}${extname}`
      continue
    }
    return filename
  }
  throw new Error('file exists.')
}

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  let filename = path.basename(imagePath)
  try {
    filename = pickFilename(path.basename(imagePath))
  } catch (e) {
    return callback(e, null)
  }

  try {
    fs.copyFileSync(imagePath, path.join(config.uploadsPath, filename))
  } catch (e) {
    return callback(e, null)
  }

  let url
  try {
    url = (new URL(filename, config.serverURL + '/uploads/')).href
  } catch (e) {
    url = config.serverURL + '/uploads/' + filename
  }

  callback(null, url)
}


================================================
FILE: lib/imageRouter/imgur.js
================================================
'use strict'
const config = require('../config')
const logger = require('../logger')

const imgur = require('@hackmd/imgur')

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  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)
    })
}


================================================
FILE: lib/imageRouter/index.js
================================================
'use strict'

const fs = require('fs')
const path = require('path')
const Router = require('express').Router
const formidable = require('formidable')

const readChunk = require('read-chunk')
const imageType = require('image-type')
const mime = require('mime-types')

const config = require('../config')
const logger = require('../logger')
const response = require('../response')

const imageRouter = module.exports = Router()

function checkImageValid (filepath) {
  try {
    const buffer = readChunk.sync(filepath, 0, 12)
    /** @type {{ ext: string, mime: string } | null} */
    const mimetypeFromBuf = imageType(buffer)
    const mimeTypeFromExt = mime.lookup(path.extname(filepath))

    return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
          mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
  } catch (err) {
    logger.error(err)
    return false
  }
}

// upload image
imageRouter.post('/uploadimage', function (req, res) {
  var form = new formidable.IncomingForm({
    keepExtensions: true
  })

  form.parse(req, function (err, fields, files) {
    if (err || !files.image || !files.image.filepath) {
      response.errorForbidden(req, res)
    } else {
      if (config.debug) {
        logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image))
      }

      if (!checkImageValid(files.image.filepath)) {
        return response.errorForbidden(req, res)
      }

      const uploadProvider = require('./' + config.imageUploadType)
      uploadProvider.uploadImage(files.image.filepath, function (err, url) {
        // remove temporary upload file, and ignore any error
        fs.unlink(files.image.filepath, () => {})
        if (err !== null) {
          logger.error(err)
          return res.status(500).end('upload image error')
        }
        res.send({
          link: url
        })
      })
    }
  })
})


================================================
FILE: lib/imageRouter/lutim.js
================================================
'use strict'
const config = require('../config')
const logger = require('../logger')

const lutim = require('lutim')

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  if (config.lutim && config.lutim.url) {
    lutim.setAPIUrl(config.lutim.url)
  }

  lutim.uploadImage(imagePath)
    .then(function (json) {
      if (config.debug) {
        logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
      }
      callback(null, lutim.getAPIUrl() + json.msg.short)
    }).catch(function (err) {
      callback(new Error(err), null)
    })
}


================================================
FILE: lib/imageRouter/minio.js
================================================
'use strict'
const fs = require('fs')
const path = require('path')

const config = require('../config')
const { getImageMimeType } = require('../utils')
const logger = require('../logger')

const Minio = require('minio')
const minioClient = new Minio.Client({
  endPoint: config.minio.endPoint,
  port: config.minio.port,
  useSSL: config.minio.secure,
  accessKey: config.minio.accessKey,
  secretKey: config.minio.secretKey
})

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  fs.readFile(imagePath, function (err, buffer) {
    if (err) {
      callback(new Error(err), null)
      return
    }

    const key = path.join('uploads', path.basename(imagePath))
    const protocol = config.minio.secure ? 'https' : 'http'

    minioClient.putObject(config.s3bucket, key, buffer, buffer.size, getImageMimeType(imagePath), function (err, data) {
      if (err) {
        callback(new Error(err), null)
        return
      }
      const hidePort = [80, 443].includes(config.minio.port)
      const urlPort = hidePort ? '' : `:${config.minio.port}`
      callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`)
    })
  })
}


================================================
FILE: lib/imageRouter/s3.js
================================================
'use strict'
const fs = require('fs')
const path = require('path')

const config = require('../config')
const { getImageMimeType } = require('../utils')
const logger = require('../logger')

const { S3Client } = require('@aws-sdk/client-s3-node/S3Client')
const { PutObjectCommand } = require('@aws-sdk/client-s3-node/commands/PutObjectCommand')

const credentials = {
  accessKeyId: config.s3.accessKeyId,
  secretAccessKey: config.s3.secretAccessKey
}

const s3 = new S3Client({
  credentials,
  region: config.s3.region,
  endpoint: config.s3.endpoint
})

exports.uploadImage = function (imagePath, callback) {
  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  fs.readFile(imagePath, function (err, buffer) {
    if (err) {
      callback(new Error(err), null)
      return
    }
    const params = {
      Bucket: config.s3bucket,
      Key: path.join('uploads', path.basename(imagePath)),
      Body: buffer,
      ACL: 'public-read'
    }
    const mimeType = getImageMimeType(imagePath)
    if (mimeType) { params.ContentType = mimeType }

    const command = new PutObjectCommand(params)

    s3.send(command).then(data => {
      // default scheme settings to https
      let s3Endpoint = 'https://s3.amazonaws.com'
      if (config.s3.region && config.s3.region !== 'us-east-1') {
        s3Endpoint = `https://s3-${config.s3.region}.amazonaws.com`
      }
      // rewrite endpoint from config
      if (config.s3.endpoint) {
        s3Endpoint = config.s3.endpoint
      }
      if (config.s3.baseURL) {
        callback(null, `${config.s3.baseURL}/${params.Key}`)
      } else {
        callback(null, `${s3Endpoint}/${config.s3bucket}/${params.Key}`)
      }
    }).catch(err => {
      if (err) {
        callback(new Error(err), null)
      }
    })
  })
}


================================================
FILE: lib/letter-avatars.js
================================================
'use strict'
// external modules
const crypto = require('crypto')
const randomcolor = require('randomcolor')
const config = require('./config')

// core
exports.generateAvatar = function (name) {
  const color = randomcolor({
    seed: name,
    luminosity: 'dark'
  })
  const letter = name.substring(0, 1).toUpperCase()

  let svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
  svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">'
  svg += '<g>'
  svg += '<rect width="96" height="96" fill="' + color + '" />'
  svg += '<text font-size="64px" font-family="sans-serif" text-anchor="middle" fill="#ffffff">'
  svg += '<tspan x="48" y="72" stroke-width=".26458px" fill="#ffffff">' + letter + '</tspan>'
  svg += '</text>'
  svg += '</g>'
  svg += '</svg>'

  return svg
}

exports.generateAvatarURL = function (name, email = '', big = true) {
  let photo
  if (typeof email !== 'string') {
    email = '' + name + '@example.com'
  }
  name = encodeURIComponent(name)

  const hash = crypto.createHash('md5')
  hash.update(email.toLowerCase())
  const hexDigest = hash.digest('hex')

  if (email !== '' && config.allowGravatar) {
    photo = 'https://www.gravatar.com/avatar/' + hexDigest
    if (big) {
      photo += '?s=400'
    } else {
      photo += '?s=96'
    }
  } else {
    photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || hexDigest) + '/avatar.svg'
  }
  return photo
}


================================================
FILE: lib/logger.js
================================================
'use strict'
const { createLogger, format, transports } = require('winston')

const logger = createLogger({
  level: 'debug',
  format: format.combine(
    format.uncolorize(),
    format.timestamp(),
    format.align(),
    format.splat(),
    format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
  ),
  transports: [
    new transports.Console({
      handleExceptions: true
    })
  ],
  exitOnError: false
})

logger.stream = {
  write: function (message, encoding) {
    logger.info(message)
  }
}

module.exports = logger


================================================
FILE: lib/metrics.js
================================================
'use strict'

const { Router } = require('express')

const { wrap } = require('./utils')

// load controller
const statusController = require('./status')
const appRouter = Router()

// register route
appRouter.get('/status', wrap(statusController.getStatus))
appRouter.get('/metrics/codimd', wrap(statusController.getMetrics))

exports.router = appRouter


================================================
FILE: lib/middleware/checkURIValid.js
================================================
'use strict'

const logger = require('../logger')
const response = require('../response')

module.exports = function (req, res, next) {
  try {
    decodeURIComponent(req.path)
  } catch (err) {
    logger.error(err)
    return response.errorBadRequest(req, res)
  }
  next()
}


================================================
FILE: lib/middleware/codiMDVersion.js
================================================
'use strict'

const config = require('../config')

module.exports = function (req, res, next) {
  res.set({
    'CodiMD-Version': config.version
  })
  return next()
}


================================================
FILE: lib/middleware/redirectWithoutTrailingSlashes.js
================================================
'use strict'

const config = require('../config')

module.exports = function (req, res, next) {
  if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {
    const queryString = req.url.slice(req.path.length)
    const urlPath = req.path.slice(0, -1)
    let serverURL = config.serverURL
    if (config.urlPath) {
      serverURL = serverURL.slice(0, -(config.urlPath.length + 1))
    }
    res.redirect(301, serverURL + urlPath + queryString)
  } else {
    next()
  }
}


================================================
FILE: lib/middleware/tooBusy.js
================================================
'use strict'

const toobusy = require('toobusy-js')

const config = require('../config')
const response = require('../response')

toobusy.maxLag(config.responseMaxLag)

module.exports = function (req, res, next) {
  if (toobusy()) {
    response.errorServiceUnavailable(req, res)
  } else {
    next()
  }
}


================================================
FILE: lib/migrations/20150504155329-create-users.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.createTable('Users', {
      id: {
        type: Sequelize.UUID,
        primaryKey: true,
        defaultValue: Sequelize.UUIDV4
      },
      profileid: {
        type: Sequelize.STRING,
        unique: true
      },
      profile: Sequelize.TEXT,
      history: Sequelize.TEXT,
      createdAt: Sequelize.DATE,
      updatedAt: Sequelize.DATE
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('Users')
  }
}


================================================
FILE: lib/migrations/20150508114741-create-notes.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.createTable('Notes', {
      id: {
        type: Sequelize.UUID,
        primaryKey: true,
        defaultValue: Sequelize.UUIDV4
      },
      ownerId: Sequelize.UUID,
      content: Sequelize.TEXT,
      title: Sequelize.STRING,
      createdAt: Sequelize.DATE,
      updatedAt: Sequelize.DATE
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('Notes')
  }
}


================================================
FILE: lib/migrations/20150515125813-create-temp.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.createTable('Temp', {
      id: {
        type: Sequelize.STRING,
        primaryKey: true
      },
      date: Sequelize.TEXT,
      createdAt: Sequelize.DATE,
      updatedAt: Sequelize.DATE
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('Temp')
  }
}


================================================
FILE: lib/migrations/20150702001020-update-to-0_3_1.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'shortid', {
      type: Sequelize.STRING,
      defaultValue: '0000000000',
      allowNull: false
    }).then(function () {
      return queryInterface.addIndex('Notes', ['shortid'], {
        indicesType: 'UNIQUE'
      })
    }).then(function () {
      return queryInterface.addColumn('Notes', 'permission', {
        type: Sequelize.STRING,
        defaultValue: 'private',
        allowNull: false
      })
    }).then(function () {
      return queryInterface.addColumn('Notes', 'viewcount', {
        type: Sequelize.INTEGER,
        defaultValue: 0
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Notes', 'viewcount')
      .then(function () {
        return queryInterface.removeColumn('Notes', 'permission')
      })
      .then(function () {
        return queryInterface.removeIndex('Notes', ['shortid'])
      })
      .then(function () {
        return queryInterface.removeColumn('Notes', 'shortid')
      })
  }
}


================================================
FILE: lib/migrations/20150915153700-change-notes-title-to-text.js
================================================
'use strict'
const isSQLite = require('../utils').isSQLite
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.changeColumn('Notes', 'title', {
      type: Sequelize.TEXT
    }).then(function () {
      if (isSQLite(queryInterface.sequelize)) {
        // manual added index will be removed in sqlite
        return queryInterface.addIndex('Notes', ['shortid'])
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.changeColumn('Notes', 'title', {
      type: Sequelize.STRING
    }).then(function () {
      if (isSQLite(queryInterface.sequelize)) {
        // manual added index will be removed in sqlite
        return queryInterface.addIndex('Notes', ['shortid'])
      }
    })
  }
}


================================================
FILE: lib/migrations/20160112220142-note-add-lastchange.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'lastchangeuserId', {
      type: Sequelize.UUID
    }).then(function () {
      return queryInterface.addColumn('Notes', 'lastchangeAt', {
        type: Sequelize.DATE
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Notes', 'lastchangeAt')
      .then(function () {
        return queryInterface.removeColumn('Notes', 'lastchangeuserId')
      })
  }
}


================================================
FILE: lib/migrations/20160420180355-note-add-alias.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'alias', {
      type: Sequelize.STRING
    }).then(function () {
      return queryInterface.addIndex('Notes', ['alias'], {
        indicesType: 'UNIQUE'
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Notes', 'alias').then(function () {
      return queryInterface.removeIndex('Notes', ['alias'])
    })
  }
}


================================================
FILE: lib/migrations/20160515114000-user-add-tokens.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () {
      return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING)
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Users', 'accessToken').then(function () {
      return queryInterface.removeColumn('Users', 'refreshToken')
    })
  }
}


================================================
FILE: lib/migrations/20160607060246-support-revision.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE).then(function () {
      return queryInterface.createTable('Revisions', {
        id: {
          type: Sequelize.UUID,
          primaryKey: true
        },
        noteId: Sequelize.UUID,
        patch: Sequelize.TEXT,
        lastContent: Sequelize.TEXT,
        content: Sequelize.TEXT,
        length: Sequelize.INTEGER,
        createdAt: Sequelize.DATE,
        updatedAt: Sequelize.DATE
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('Revisions').then(function () {
      return queryInterface.removeColumn('Notes', 'savedAt')
    })
  }
}


================================================
FILE: lib/migrations/20160703062241-support-authorship.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT).then(function () {
      return queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT)
    }).then(function () {
      return queryInterface.createTable('Authors', {
        id: {
          type: Sequelize.INTEGER,
          primaryKey: true,
          autoIncrement: true
        },
        color: Sequelize.STRING,
        noteId: Sequelize.UUID,
        userId: Sequelize.UUID,
        createdAt: Sequelize.DATE,
        updatedAt: Sequelize.DATE
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('Authors').then(function () {
      return queryInterface.removeColumn('Revisions', 'authorship')
    }).then(function () {
      return queryInterface.removeColumn('Notes', 'authorship')
    })
  }
}


================================================
FILE: lib/migrations/20161009040430-support-delete-note.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Notes', 'deletedAt')
  }
}


================================================
FILE: lib/migrations/20161201050312-support-email-signin.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () {
      return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) {
        if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') {
          console.log('Migration has already run… ignoring.')
        } else {
          throw error
        }
      })
    }).catch(function (error) {
      if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') {
        console.log('Migration has already run… ignoring.')
      } else {
        throw error
      }
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Users', 'email').then(function () {
      return queryInterface.removeColumn('Users', 'password')
    })
  }
}


================================================
FILE: lib/migrations/20171009121200-longtext-for-mysql.js
================================================
'use strict'
module.exports = {
  up: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
    await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
    await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
    await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
  },

  down: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
    await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
    await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
    await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
  }
}


================================================
FILE: lib/migrations/20180209120907-longtext-of-authorship.js
================================================
'use strict'

module.exports = {
  up: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
    await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
  },

  down: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
    await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
  }
}


================================================
FILE: lib/migrations/20180306150303-fix-enum.js
================================================
'use strict'

module.exports = {
  up: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
  },

  down: async function (queryInterface, Sequelize) {
    await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
  }
}


================================================
FILE: lib/migrations/20180326103000-use-text-in-tokens.js
================================================
'use strict'

module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.changeColumn('Users', 'accessToken', {
      type: Sequelize.TEXT
    }).then(function () {
      return queryInterface.changeColumn('Users', 'refreshToken', {
        type: Sequelize.TEXT
      })
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.changeColumn('Users', 'accessToken', {
      type: Sequelize.STRING
    }).then(function () {
      return queryInterface.changeColumn('Users', 'refreshToken', {
        type: Sequelize.STRING
      })
    })
  }
}


================================================
FILE: lib/migrations/20180525153000-user-add-delete-token.js
================================================
'use strict'
module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.addColumn('Users', 'deleteToken', {
      type: Sequelize.UUID,
      defaultValue: Sequelize.UUIDV4
    })
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.removeColumn('Users', 'deleteToken')
  }
}


================================================
FILE: lib/migrations/20200104215332-remove-temp-table.js
================================================
'use strict'

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Temp')
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Temp', {
      id: {
        type: Sequelize.STRING,
        primaryKey: true
      },
      date: Sequelize.TEXT,
      createdAt: Sequelize.DATE,
      updatedAt: Sequelize.DATE
    })
  }
}


================================================
FILE: lib/migrations/20240114120250-revision-add-index.js
================================================
'use strict'

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.addIndex('Revisions', ['noteId'], {})
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.removeIndex('Revisions', 'noteId')
  }
}


================================================
FILE: lib/models/author.js
================================================
'use strict'
// external modules
var Sequelize = require('sequelize')

module.exports = function (sequelize, DataTypes) {
  var Author = sequelize.define('Author', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    color: {
      type: DataTypes.STRING
    }
  }, {
    indexes: [
      {
        unique: true,
        fields: ['noteId', 'userId']
      }
    ]
  })

  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
}


================================================
FILE: lib/models/index.js
================================================
'use strict'
// external modules
var fs = require('fs')
var path = require('path')
var Sequelize = require('sequelize')
const { cloneDeep } = require('lodash')

// core
var config = require('../config')
var logger = require('../logger')

var dbconfig = cloneDeep(config.db)
dbconfig.logging = config.debug ? (data) => {
  logger.info(data)
} : false

var sequelize = null

// Heroku specific
if (config.dbURL) {
  sequelize = new Sequelize(config.dbURL, dbconfig)
} else {
  sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig)
}

// [Postgres] Handling NULL bytes
// https://github.com/sequelize/sequelize/issues/6485
function stripNullByte (value) {
  value = '' + value
  // eslint-disable-next-line no-control-regex
  return value ? value.replace(/\u0000/g, '') : value
}
sequelize.stripNullByte = stripNullByte

function processData (data, _default, process) {
  if (data === undefined) return data
  else return data === null ? _default : (process ? process(data) : data)
}
sequelize.processData = processData

var db = {}

fs.readdirSync(__dirname)
  .filter(function (file) {
    return (file.indexOf('.') !== 0) && (file !== 'index.js')
  })
  .forEach(function (file) {
    var model = require(path.join(__dirname, file))(sequelize, Sequelize)
    db[model.name] = model
  })

Object.keys(db).forEach(function (modelName) {
  if ('associate' in db[modelName]) {
    db[modelName].associate(db)
  }
})

db.sequelize = sequelize
db.Sequelize = Sequelize

module.exports = db


================================================
FILE: lib/models/note.js
================================================
'use strict'
// external modules
var fs = require('fs')
var path = require('path')
var LZString = require('@hackmd/lz-string')
var base64url = require('base64url')
var md = require('markdown-it')()
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('@hackmd/diff-match-patch')
var dmp = new DiffMatchPatch()

const { stripTags } = require('../../utils/string')

// core
var config = require('../config')
var logger = require('../logger')

// ot
var ot = require('../ot')

// permission types
var permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private']

module.exports = function (sequelize, DataTypes) {
  var Note = sequelize.define('Note', {
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
      defaultValue: Sequelize.UUIDV4
    },
    shortid: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false,
      defaultValue: shortId.generate
    },
    alias: {
      type: DataTypes.STRING,
      unique: true
    },
    permission: {
      type: DataTypes.ENUM,
      values: permissionTypes
    },
    viewcount: {
      type: DataTypes.INTEGER,
      allowNull: false,
      defaultValue: 0
    },
    title: {
      type: DataTypes.TEXT,
      get: function () {
        return sequelize.processData(this.getDataValue('title'), '')
      },
      set: function (value) {
        this.setDataValue('title', sequelize.stripNullByte(value))
      }
    },
    content: {
      type: DataTypes.TEXT('long'),
      get: function () {
        return sequelize.processData(this.getDataValue('content'), '')
      },
      set: function (value) {
        this.setDataValue('content', sequelize.stripNullByte(value))
      }
    },
    authorship: {
      type: DataTypes.TEXT('long'),
      get: function () {
        return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse)
      },
      set: function (value) {
        this.setDataValue('authorship', JSON.stringify(value))
      }
    },
    lastchangeAt: {
      type: DataTypes.DATE
    },
    savedAt: {
      type: DataTypes.DATE
    }
  }, {
    paranoid: false,
    hooks: {
      beforeCreate: function (note, options) {
        return new Promise(function (resolve, reject) {
          // if no content specified then use default note
          if (!note.content) {
            let filePath = config.default
Download .txt
gitextract_04c_nojh/

├── .buildpacks
├── .devcontainer/
│   ├── Dockerfile
│   ├── devcontainer.json
│   └── docker-compose.yml
├── .dockerignore
├── .editorconfig
├── .github/
│   ├── tests/
│   │   ├── README.md
│   │   └── pull-request.json
│   └── workflows/
│       ├── build.yml
│       ├── check-release.yml
│       └── push-image.yml
├── .gitignore
├── .mailmap
├── .nvmrc
├── .sequelizerc.example
├── AUTHORS
├── Aptfile
├── CONTRIBUTING.md
├── FUNDING.json
├── LICENSE
├── Procfile
├── README.md
├── app.js
├── app.json
├── babel.config.js
├── bin/
│   ├── heroku
│   ├── heroku_start.sh
│   ├── manage_users
│   └── setup
├── config.js
├── config.json.example
├── contribute/
│   └── developer-certificate-of-origin
├── deployments/
│   ├── Dockerfile
│   ├── build.sh
│   ├── docker-compose.yml
│   └── docker-entrypoint.sh
├── lib/
│   ├── auth/
│   │   ├── bitbucket/
│   │   │   └── index.js
│   │   ├── dropbox/
│   │   │   └── index.js
│   │   ├── email/
│   │   │   └── index.js
│   │   ├── facebook/
│   │   │   └── index.js
│   │   ├── github/
│   │   │   └── index.js
│   │   ├── gitlab/
│   │   │   └── index.js
│   │   ├── google/
│   │   │   └── index.js
│   │   ├── index.js
│   │   ├── ldap/
│   │   │   └── index.js
│   │   ├── mattermost/
│   │   │   └── index.js
│   │   ├── oauth2/
│   │   │   ├── index.js
│   │   │   └── strategy.js
│   │   ├── openid/
│   │   │   └── index.js
│   │   ├── saml/
│   │   │   └── index.js
│   │   ├── twitter/
│   │   │   └── index.js
│   │   └── utils.js
│   ├── config/
│   │   ├── default.js
│   │   ├── defaultSSL.js
│   │   ├── dockerSecret.js
│   │   ├── enum.js
│   │   ├── environment.js
│   │   ├── index.js
│   │   └── utils.js
│   ├── csp.js
│   ├── errorPage/
│   │   └── index.js
│   ├── history/
│   │   └── index.js
│   ├── homepage/
│   │   └── index.js
│   ├── imageRouter/
│   │   ├── azure.js
│   │   ├── filesystem.js
│   │   ├── imgur.js
│   │   ├── index.js
│   │   ├── lutim.js
│   │   ├── minio.js
│   │   └── s3.js
│   ├── letter-avatars.js
│   ├── logger.js
│   ├── metrics.js
│   ├── middleware/
│   │   ├── checkURIValid.js
│   │   ├── codiMDVersion.js
│   │   ├── redirectWithoutTrailingSlashes.js
│   │   └── tooBusy.js
│   ├── migrations/
│   │   ├── 20150504155329-create-users.js
│   │   ├── 20150508114741-create-notes.js
│   │   ├── 20150515125813-create-temp.js
│   │   ├── 20150702001020-update-to-0_3_1.js
│   │   ├── 20150915153700-change-notes-title-to-text.js
│   │   ├── 20160112220142-note-add-lastchange.js
│   │   ├── 20160420180355-note-add-alias.js
│   │   ├── 20160515114000-user-add-tokens.js
│   │   ├── 20160607060246-support-revision.js
│   │   ├── 20160703062241-support-authorship.js
│   │   ├── 20161009040430-support-delete-note.js
│   │   ├── 20161201050312-support-email-signin.js
│   │   ├── 20171009121200-longtext-for-mysql.js
│   │   ├── 20180209120907-longtext-of-authorship.js
│   │   ├── 20180306150303-fix-enum.js
│   │   ├── 20180326103000-use-text-in-tokens.js
│   │   ├── 20180525153000-user-add-delete-token.js
│   │   ├── 20200104215332-remove-temp-table.js
│   │   └── 20240114120250-revision-add-index.js
│   ├── models/
│   │   ├── author.js
│   │   ├── index.js
│   │   ├── note.js
│   │   ├── revision.js
│   │   └── user.js
│   ├── note/
│   │   ├── index.js
│   │   └── noteActions.js
│   ├── ot/
│   │   ├── client.js
│   │   ├── editor-socketio-server.js
│   │   ├── index.js
│   │   ├── selection.js
│   │   ├── server.js
│   │   ├── simple-text-operation.js
│   │   ├── text-operation.js
│   │   └── wrapped-operation.js
│   ├── realtime/
│   │   ├── processQueue.js
│   │   ├── realtime.js
│   │   ├── realtimeCleanDanglingUserJob.js
│   │   ├── realtimeClientConnection.js
│   │   ├── realtimeSaveRevisionJob.js
│   │   └── realtimeUpdateDirtyNoteJob.js
│   ├── response.js
│   ├── routes.js
│   ├── status/
│   │   └── index.js
│   ├── user/
│   │   └── index.js
│   ├── utils.js
│   ├── web/
│   │   └── middleware/
│   │       └── checkVersion.js
│   └── workers/
│       └── dmpWorker.js
├── locales/
│   ├── ca.json
│   ├── da.json
│   ├── de.json
│   ├── el.json
│   ├── en.json
│   ├── eo.json
│   ├── es.json
│   ├── fr.json
│   ├── hi.json
│   ├── hr.json
│   ├── id.json
│   ├── it.json
│   ├── ja.json
│   ├── ko.json
│   ├── nl.json
│   ├── pl.json
│   ├── pt.json
│   ├── ru.json
│   ├── sr.json
│   ├── sv.json
│   ├── tr.json
│   ├── uk.json
│   ├── zh-CN.json
│   └── zh-TW.json
├── package.json
├── public/
│   ├── .eslintrc.js
│   ├── css/
│   │   ├── bootstrap-social.css
│   │   ├── center.css
│   │   ├── codemirror-extend/
│   │   │   ├── ayu-dark.css
│   │   │   ├── ayu-mirage.css
│   │   │   ├── one-dark.css
│   │   │   ├── tomorrow-night-bright.css
│   │   │   └── tomorrow-night-eighties.css
│   │   ├── cover.css
│   │   ├── extra.css
│   │   ├── font.css
│   │   ├── github-extract.css
│   │   ├── google-font.css
│   │   ├── index.css
│   │   ├── markdown.css
│   │   ├── mermaid.css
│   │   ├── site.css
│   │   ├── slide-preview.css
│   │   └── slide.css
│   ├── default.md
│   ├── docs/
│   │   ├── features.md
│   │   ├── privacy.md.example
│   │   ├── release-notes.md
│   │   ├── slide-example.md
│   │   └── yaml-metadata.md
│   ├── js/
│   │   ├── cover.js
│   │   ├── extra.js
│   │   ├── history.js
│   │   ├── htmlExport.js
│   │   ├── index.js
│   │   ├── lib/
│   │   │   ├── appState.js
│   │   │   ├── common/
│   │   │   │   ├── constant.ejs
│   │   │   │   ├── login.js
│   │   │   │   └── metrics.ejs
│   │   │   ├── config/
│   │   │   │   └── index.js
│   │   │   ├── editor/
│   │   │   │   ├── config.js
│   │   │   │   ├── constants.js
│   │   │   │   ├── index.js
│   │   │   │   ├── markdown-lint/
│   │   │   │   │   └── index.js
│   │   │   │   ├── spellcheck.js
│   │   │   │   ├── statusbar.html
│   │   │   │   ├── table-editor.js
│   │   │   │   ├── toolbar.html
│   │   │   │   ├── ui-elements.js
│   │   │   │   └── utils.js
│   │   │   ├── markdown/
│   │   │   │   └── utils.js
│   │   │   ├── modeType.js
│   │   │   ├── renderer/
│   │   │   │   ├── csvpreview.js
│   │   │   │   ├── fretboard/
│   │   │   │   │   ├── css/
│   │   │   │   │   │   └── i.css
│   │   │   │   │   └── fretboard.js
│   │   │   │   └── lightbox/
│   │   │   │       ├── index.js
│   │   │   │       └── lightbox.css
│   │   │   └── syncscroll.js
│   │   ├── locale.js
│   │   ├── mathjax-config-extra.js
│   │   ├── pretty.js
│   │   ├── render.js
│   │   ├── reveal-markdown.js
│   │   ├── revealjs-plugins/
│   │   │   ├── elapsed-time-bar/
│   │   │   │   └── elapsed-time-bar.js
│   │   │   └── spotlight/
│   │   │       └── spotlight.js
│   │   ├── slide.js
│   │   └── utils.js
│   ├── markdown-lint/
│   │   └── css/
│   │       └── lint.css
│   ├── uploads/
│   │   └── .gitkeep
│   ├── vendor/
│   │   ├── abcjs_basic_3.1.1-min.js
│   │   ├── codemirror-spell-checker/
│   │   │   ├── en_US.aff
│   │   │   └── en_US.dic
│   │   ├── inlineAttachment/
│   │   │   ├── codemirror.inline-attachment.js
│   │   │   └── inline-attachment.js
│   │   ├── jquery-textcomplete/
│   │   │   └── jquery.textcomplete.js
│   │   ├── md-toc.js
│   │   ├── ot/
│   │   │   ├── ajax-adapter.js
│   │   │   ├── client.js
│   │   │   ├── codemirror-adapter.js
│   │   │   ├── compress.sh
│   │   │   ├── editor-client.js
│   │   │   ├── selection.js
│   │   │   ├── socketio-adapter.js
│   │   │   ├── text-operation.js
│   │   │   ├── undo-manager.js
│   │   │   └── wrapped-operation.js
│   │   └── showup/
│   │       ├── showup.css
│   │       └── showup.js
│   └── views/
│       ├── codimd/
│       │   ├── body.ejs
│       │   ├── foot.ejs
│       │   ├── footer.ejs
│       │   ├── head.ejs
│       │   └── header.ejs
│       ├── codimd.ejs
│       ├── error.ejs
│       ├── html.hbs
│       ├── includes/
│       │   ├── header.ejs
│       │   └── scripts.ejs
│       ├── index/
│       │   ├── body.ejs
│       │   ├── foot.ejs
│       │   ├── footer.ejs
│       │   ├── head.ejs
│       │   └── header.ejs
│       ├── index.ejs
│       ├── pretty.ejs
│       ├── shared/
│       │   ├── disqus.ejs
│       │   ├── ga.ejs
│       │   ├── help-modal.ejs
│       │   ├── pandoc-export-modal.ejs
│       │   ├── polyfill.ejs
│       │   ├── refresh-modal.ejs
│       │   ├── revision-modal.ejs
│       │   └── signin-modal.ejs
│       └── slide.ejs
├── scalingo.json
├── test/
│   ├── auth/
│   │   └── oauth2/
│   │       └── strategy.test.js
│   ├── connectionQueue.test.js
│   ├── csp.js
│   ├── letter-avatars.js
│   ├── realtime/
│   │   ├── cleanDanglingUser.test.js
│   │   ├── connection.test.js
│   │   ├── dirtyNoteUpdate.test.js
│   │   ├── disconnect-process.test.js
│   │   ├── extractNoteIdFromSocket.test.js
│   │   ├── ifMayEdit.test.js
│   │   ├── parseNoteIdFromSocket.test.js
│   │   ├── realtime.test.js
│   │   ├── saveRevisionJob.test.js
│   │   ├── socket-events.test.js
│   │   ├── updateNote.test.js
│   │   └── utils.js
│   └── testDoubles/
│       ├── ProcessQueueFake.js
│       ├── loggerFake.js
│       ├── otFake.js
│       └── realtimeJobStub.js
├── utils/
│   └── string.js
├── webpack.common.js
├── webpack.dev.js
├── webpack.htmlexport.js
└── webpack.prod.js
Download .txt
SYMBOL INDEX (679 symbols across 71 files)

FILE: app.js
  function createHttpServer (line 32) | function createHttpServer () {
  function startListen (line 255) | function startListen () {
  function handleTermSignals (line 301) | function handleTermSignals () {

FILE: lib/auth/github/index.js
  function githubUrl (line 18) | function githubUrl (path) {

FILE: lib/auth/oauth2/strategy.js
  function parseProfile (line 6) | function parseProfile (data) {
  function extractProfileAttribute (line 26) | function extractProfileAttribute (data, path) {
  function checkAuthorization (line 45) | function checkAuthorization (data, done) {
  class OAuth2CustomStrategy (line 55) | class OAuth2CustomStrategy extends Strategy {
    method constructor (line 56) | constructor (options, verify) {
    method userProfile (line 64) | userProfile (accessToken, done) {

FILE: lib/config/defaultSSL.js
  function getFile (line 5) | function getFile (path) {

FILE: lib/config/dockerSecret.js
  function getSecret (line 8) | function getSecret (secret) {

FILE: lib/csp.js
  function mergeDirectives (line 55) | function mergeDirectives (existingDirectives, newDirectives) {
  function mergeDirectivesIf (line 65) | function mergeDirectivesIf (condition, existingDirectives, newDirectives) {
  function areAllInlineScriptsAllowed (line 71) | function areAllInlineScriptsAllowed (directives) {
  function addInlineScriptExceptions (line 75) | function addInlineScriptExceptions (directives) {
  function getCspNonce (line 82) | function getCspNonce (req, res) {
  function addUpgradeUnsafeRequestsOptionTo (line 86) | function addUpgradeUnsafeRequestsOptionTo (directives) {
  function addReportURI (line 94) | function addReportURI (directives) {

FILE: lib/history/index.js
  function getHistory (line 12) | function getHistory (userid, callback) {
  function setHistory (line 61) | function setHistory (userid, history, callback) {
  function updateHistory (line 76) | function updateHistory (userid, noteId, document, time) {
  function parseHistoryToArray (line 98) | function parseHistoryToArray (history) {
  function parseHistoryToObject (line 107) | function parseHistoryToObject (history) {
  function historyGet (line 116) | function historyGet (req, res) {
  function historyPost (line 130) | function historyPost (req, res) {
  function historyDelete (line 171) | function historyDelete (req, res) {

FILE: lib/imageRouter/filesystem.js
  constant URL (line 5) | const URL = require('url').URL
  function randomFilename (line 14) | function randomFilename () {
  function pickFilename (line 23) | function pickFilename (defaultFilename) {

FILE: lib/imageRouter/index.js
  function checkImageValid (line 18) | function checkImageValid (filepath) {

FILE: lib/models/index.js
  function stripNullByte (line 28) | function stripNullByte (value) {
  function processData (line 35) | function processData (data, _default, process) {

FILE: lib/models/note.js
  function syncNote (line 202) | async function syncNote (noteInFS, note) {
  function readFileSystemNote (line 587) | function readFileSystemNote (filePath) {
  function shouldSyncNote (line 598) | function shouldSyncNote (note, noteInFS) {

FILE: lib/models/revision.js
  function createDmpWorker (line 20) | function createDmpWorker () {
  function sendDmpWorker (line 47) | function sendDmpWorker (data, callback) {

FILE: lib/note/index.js
  function getNoteById (line 12) | async function getNoteById (noteId, { includeUser } = { includeUser: fal...
  function createNote (line 36) | async function createNote (userId, noteAlias) {
  function showNote (line 54) | async function showNote (req, res) {
  function canViewNote (line 82) | function canViewNote (note, isLogin, userId) {
  function showPublishNote (line 92) | async function showPublishNote (req, res) {
  function noteActions (line 146) | async function noteActions (req, res) {
  function getMyNoteList (line 194) | async function getMyNoteList (userId, callback) {
  function listMyNotes (line 222) | function listMyNotes (req, res) {

FILE: lib/note/noteActions.js
  function actionPublish (line 16) | function actionPublish (req, res, note) {
  function actionSlide (line 20) | function actionSlide (req, res, note) {
  function actionDownload (line 24) | function actionDownload (req, res, note) {
  function actionInfo (line 40) | function actionInfo (req, res, note) {
  function actionPDF (line 67) | function actionPDF (req, res, note) {
  function actionPandoc (line 120) | async function actionPandoc (req, res, note) {
  function actionGist (line 169) | function actionGist (req, res, note) {
  function actionRevision (line 180) | function actionRevision (req, res, note) {

FILE: lib/ot/client.js
  function Client (line 11) | function Client (revision) {
  function Synchronized (line 65) | function Synchronized () {}
  function AwaitingConfirm (line 99) | function AwaitingConfirm (outstanding) {
  function AwaitingWithBuffer (line 154) | function AwaitingWithBuffer (outstanding, buffer) {
  function Stale (line 218) | function Stale(acknowlaged, client, revision) {
  function StaleWithBuffer (line 259) | function StaleWithBuffer(acknowlaged, buffer, client, revision) {

FILE: lib/ot/editor-socketio-server.js
  function EditorSocketIOServer (line 12) | function EditorSocketIOServer(document, operations, docId, mayWrite, ope...
  function extend (line 26) | function extend(target, source) {

FILE: lib/ot/selection.js
  function Range (line 15) | function Range (anchor, head) {
  function transformIndex (line 33) | function transformIndex (index) {
  function Selection (line 60) | function Selection (ranges) {

FILE: lib/ot/server.js
  function Server (line 12) | function Server (document, operations) {

FILE: lib/ot/simple-text-operation.js
  function SimpleTextOperation (line 10) | function SimpleTextOperation () {}
  function Insert (line 14) | function Insert (str, position) {
  function Delete (line 42) | function Delete (count, position) {
  function Noop (line 70) | function Noop () {

FILE: lib/ot/text-operation.js
  function TextOperation (line 10) | function TextOperation () {
  function getSimpleOp (line 337) | function getSimpleOp (operation, fn) {
  function getStartIndex (line 351) | function getStartIndex (operation) {

FILE: lib/ot/wrapped-operation.js
  function WrappedOperation (line 10) | function WrappedOperation (operation, meta) {
  function copy (line 29) | function copy (source, target) {
  function composeMeta (line 37) | function composeMeta (a, b) {
  function transformMeta (line 55) | function transformMeta (meta, operation) {

FILE: lib/realtime/processQueue.js
  class ProcessQueue (line 15) | class ProcessQueue extends EventEmitter {
    method constructor (line 16) | constructor ({
    method onEventProcessFunc (line 40) | onEventProcessFunc () {
    method start (line 48) | start () {
    method stop (line 55) | stop () {
    method checkTaskIsInQueue (line 62) | checkTaskIsInQueue (id) {
    method push (line 72) | push (id, processingFunc) {
    method process (line 86) | process () {

FILE: lib/realtime/realtime.js
  function onAuthorizeSuccess (line 48) | function onAuthorizeSuccess (data, accept) {
  function onAuthorizeFail (line 53) | function onAuthorizeFail (data, message, error, accept) {
  function secure (line 59) | function secure (socket, next) {
  function emitCheck (line 85) | function emitCheck (note) {
  function getNotePool (line 101) | function getNotePool () {
  function isNoteExistsInPool (line 105) | function isNoteExistsInPool (noteId) {
  function addNote (line 109) | function addNote (note) {
  function getNotePoolSize (line 115) | function getNotePoolSize () {
  function deleteNoteFromPool (line 119) | function deleteNoteFromPool (noteId) {
  function deleteAllNoteFromPool (line 123) | function deleteAllNoteFromPool () {
  function getNoteFromNotePool (line 129) | function getNoteFromNotePool (noteId) {
  function getUserPool (line 133) | function getUserPool () {
  function getUserFromUserPool (line 137) | function getUserFromUserPool (userId) {
  function disconnectSocketOnNote (line 146) | function disconnectSocketOnNote (note) {
  function updateNote (line 157) | function updateNote (note, callback) {
  function findNoteByIdAsync (line 166) | function findNoteByIdAsync (id) {
  function updateHistoryForEveryUserCollaborateNote (line 174) | function updateHistoryForEveryUserCollaborateNote (note) {
  function getUserProfileByIdAsync (line 184) | async function getUserProfileByIdAsync (id) {
  class UserNotFoundException (line 194) | class UserNotFoundException extends Error {
    method constructor (line 195) | constructor () {
  function getLastChangeUserProfileAsync (line 202) | async function getLastChangeUserProfileAsync (currentLastChangeUserId, l...
  function buildNoteUpdateData (line 212) | function buildNoteUpdateData (note) {
  function _updateNoteAsync (line 224) | async function _updateNoteAsync (note) {
  function getStatus (line 250) | function getStatus () {
  function isReady (line 311) | function isReady () {
  function parseUrl (line 318) | function parseUrl (data) {
  function extractNoteIdFromSocket (line 332) | function extractNoteIdFromSocket (socket) {
  function parseNoteIdFromSocketAsync (line 365) | async function parseNoteIdFromSocketAsync (socket) {
  function emitOnlineUsers (line 385) | function emitOnlineUsers (socket) {
  function emitUserStatus (line 402) | function emitUserStatus (socket) {
  function emitRefresh (line 411) | function emitRefresh (socket) {
  function checkViewPermission (line 431) | function checkViewPermission (req, note) {
  function fetchFullNoteAsync (line 450) | async function fetchFullNoteAsync (noteId) {
  function buildAuthorProfilesFromNote (line 472) | function buildAuthorProfilesFromNote (noteAuthors) {
  function makeNewServerNote (line 488) | function makeNewServerNote (note) {
  function failConnection (line 512) | function failConnection (code, err, socket) {
  function queueForDisconnect (line 521) | function queueForDisconnect (socket) {
  function buildUserOutData (line 562) | function buildUserOutData (user) {
  function updateUserData (line 578) | function updateUserData (socket, user) {
  function canEditNote (line 593) | function canEditNote (notePermission, noteOwnerId, currentUserId) {
  function ifMayEdit (line 609) | function ifMayEdit (socket, callback) {
  function operationCallback (line 626) | function operationCallback (socket, operation) {
  function updateHistory (line 669) | function updateHistory (userId, note, time) {
  function getUniqueColorPerNote (line 674) | function getUniqueColorPerNote (noteId, maxAttempt = 10) {
  function queueForConnect (line 696) | function queueForConnect (socket) {
  function connection (line 795) | function connection (socket) {
  function terminate (line 801) | function terminate () {

FILE: lib/realtime/realtimeCleanDanglingUserJob.js
  class CleanDanglingUserJob (line 10) | class CleanDanglingUserJob {
    method constructor (line 11) | constructor (realtime) {
    method start (line 15) | start () {
    method stop (line 20) | stop () {
    method cleanDanglingUser (line 26) | cleanDanglingUser () {

FILE: lib/realtime/realtimeClientConnection.js
  class RealtimeClientConnection (line 9) | class RealtimeClientConnection {
    method constructor (line 10) | constructor (socket) {
    method registerEventHandler (line 15) | registerEventHandler () {
    method isUserLoggedIn (line 40) | isUserLoggedIn () {
    method isNoteAndUserExists (line 44) | isNoteAndUserExists () {
    method isNoteOwner (line 50) | isNoteOwner () {
    method isAnonymousEnable (line 55) | isAnonymousEnable () {
    method getAvailablePermissions (line 60) | getAvailablePermissions () {
    method getCurrentUser (line 73) | getCurrentUser () {
    method getCurrentLoggedInUserId (line 78) | getCurrentLoggedInUserId () {
    method getCurrentNote (line 82) | getCurrentNote () {
    method getNoteChannel (line 87) | getNoteChannel () {
    method destroyNote (line 91) | async destroyNote (id) {
    method changeNotePermission (line 97) | async changeNotePermission (newPermission) {
    method notifyPermissionChanged (line 110) | notifyPermissionChanged () {
    method refreshEventHandler (line 128) | refreshEventHandler () {
    method checkVersionEventHandler (line 132) | checkVersionEventHandler () {
    method userStatusEventHandler (line 139) | userStatusEventHandler (data) {
    method userChangedEventHandler (line 152) | userChangedEventHandler () {
    method onlineUsersEventHandler (line 164) | onlineUsersEventHandler () {
    method cursorFocusEventHandler (line 177) | cursorFocusEventHandler (data) {
    method cursorActivityEventHandler (line 185) | cursorActivityEventHandler (data) {
    method cursorBlurEventHandler (line 193) | cursorBlurEventHandler () {
    method deleteNoteEventHandler (line 202) | deleteNoteEventHandler () {
    method permissionChangeEventHandler (line 220) | permissionChangeEventHandler (permission) {
    method disconnectEventHandler (line 237) | disconnectEventHandler () {

FILE: lib/realtime/realtimeSaveRevisionJob.js
  class SaveRevisionJob (line 9) | class SaveRevisionJob {
    method constructor (line 10) | constructor (realtime) {
    method start (line 15) | start () {
    method stop (line 20) | stop () {
    method saveRevision (line 26) | saveRevision () {
    method getSaverSleep (line 36) | getSaverSleep () {
    method setSaverSleep (line 40) | setSaverSleep (val) {

FILE: lib/realtime/realtimeUpdateDirtyNoteJob.js
  class UpdateDirtyNoteJob (line 7) | class UpdateDirtyNoteJob {
    method constructor (line 8) | constructor (realtime) {
    method start (line 12) | start () {
    method stop (line 17) | stop () {
    method updateDirtyNotes (line 23) | updateDirtyNotes () {
    method updateDirtyNote (line 34) | async updateDirtyNote (note) {
    method updateNoteAsync (line 66) | updateNoteAsync (note) {

FILE: lib/response.js
  function errorForbidden (line 31) | function errorForbidden (req, res) {
  function errorNotFound (line 42) | function errorNotFound (req, res) {
  function errorBadRequest (line 46) | function errorBadRequest (req, res) {
  function errorTooLong (line 50) | function errorTooLong (req, res) {
  function errorInternalError (line 54) | function errorInternalError (req, res) {
  function errorServiceUnavailable (line 58) | function errorServiceUnavailable (req, res) {
  function responseError (line 62) | function responseError (res, code, detail, msg) {
  function responseCodiMD (line 71) | function responseCodiMD (res, note) {
  function updateHistory (line 86) | function updateHistory (userId, note, document, time) {
  function newNote (line 92) | function newNote (req, res, next) {
  function newCheckViewPermission (line 122) | function newCheckViewPermission (note, isLogin, userId) {
  function checkViewPermission (line 132) | function checkViewPermission (req, note) {
  function findNote (line 142) | function findNote (req, res, callback, include) {
  function actionDownload (line 176) | function actionDownload (req, res, note) {
  function publishNoteActions (line 193) | function publishNoteActions (req, res, next) {
  function publishSlideActions (line 210) | function publishSlideActions (req, res, next) {
  function githubActions (line 224) | function githubActions (req, res, next) {
  function githubActionGist (line 239) | function githubActionGist (req, res, note) {
  function gitlabActions (line 296) | function gitlabActions (req, res, next) {
  function gitlabActionProjects (line 311) | function gitlabActionProjects (req, res, note) {
  function showPublishSlide (line 342) | function showPublishSlide (req, res, next) {

FILE: lib/web/middleware/checkVersion.js
  constant VERSION_CHECK_ENDPOINT (line 12) | const VERSION_CHECK_ENDPOINT = 'https://evangelion.codimd.dev/'
  constant CHECK_TIMEOUT (line 13) | const CHECK_TIMEOUT = 1000 * 60 * 60 * 24 // 1 day
  function checkVersion (line 21) | async function checkVersion (ctx) {

FILE: lib/workers/dmpWorker.js
  function createPatch (line 58) | function createPatch (lastDoc, currDoc) {
  function getRevision (line 71) | function getRevision (revisions, count) {

FILE: public/js/cover.js
  function pageInit (line 68) | function pageInit () {
  function checkHistoryList (line 117) | function checkHistoryList () {
  function parseHistoryCallback (line 133) | function parseHistoryCallback (list, notehistory) {
  function historyCloseClick (line 207) | function historyCloseClick (e) {
  function historyPinClick (line 217) | function historyPinClick (e) {
  function updateItemFromNow (line 256) | function updateItemFromNow () {
  function deleteHistory (line 268) | function deleteHistory () {
  method data (line 387) | data () {
  function buildTagsFilter (line 396) | function buildTagsFilter (tags) {

FILE: public/js/extra.js
  function updateLastChange (line 70) | function updateLastChange () {
  function updateLastChangeUser (line 88) | function updateLastChangeUser () {
  function updateOwner (line 106) | function updateOwner () {
  function getTitle (line 121) | function getTitle (view) {
  function renderTitle (line 137) | function renderTitle (view) {
  function renderFilename (line 148) | function renderFilename (view) {
  function renderTags (line 157) | function renderTags (view) {
  function slugifyWithUTF8 (line 190) | function slugifyWithUTF8 (text) {
  function parseMeta (line 201) | function parseMeta (md, edit, view, toc, tocAffix) {
  function replaceExtraTags (line 251) | function replaceExtraTags (html) {
  function jsonp (line 269) | function jsonp (url, callback) {
  function finishView (line 287) | function finishView (view) {
  function postProcess (line 760) | function postProcess (code) {
  function removeDOMEvents (line 805) | function removeDOMEvents (view) {
  function generateCleanHTML (line 812) | function generateCleanHTML (view) {
  function exportToRawHTML (line 851) | function exportToRawHTML (view) {
  function exportToHTML (line 863) | function exportToHTML (view) {
  function toggleTodoEvent (line 914) | function toggleTodoEvent (e) {
  function removeHash (line 933) | function removeHash () {
  function checkExpandToggle (line 939) | function checkExpandToggle () {
  function generateToc (line 955) | function generateToc (id) {
  function smoothHashScroll (line 1000) | function smoothHashScroll () {
  function imgPlayiframe (line 1030) | function imgPlayiframe (element, src) {
  function autoLinkify (line 1093) | function autoLinkify (view) {
  function getHeaderContent (line 1103) | function getHeaderContent (header) {
  function changeHeaderId (line 1110) | function changeHeaderId ($header, id, newId) {
  function deduplicatedHeaderId (line 1117) | function deduplicatedHeaderId (view) {
  function renderTOC (line 1152) | function renderTOC (view) {
  function scrollToHash (line 1183) | function scrollToHash () {
  function highlightRender (line 1201) | function highlightRender (code, lang) {
  function renderContainer (line 1272) | function renderContainer (tokens, idx, options, env, self) {
  function get (line 1488) | function get (state, line) {
  function meta (line 1494) | function meta (state, start, end, silent) {
  function metaPlugin (line 1523) | function metaPlugin (md) {
  function sanitizeMarkmapNode (line 1545) | function sanitizeMarkmapNode (node) {

FILE: public/js/history.js
  function saveHistory (line 20) | function saveHistory (notehistory) {
  function saveHistoryToStorage (line 31) | function saveHistoryToStorage (notehistory) {
  function saveHistoryToServer (line 35) | function saveHistoryToServer (notehistory) {
  function saveStorageHistoryToServer (line 41) | function saveStorageHistoryToServer (callback) {
  function clearDuplicatedHistory (line 53) | function clearDuplicatedHistory (notehistory) {
  function addHistory (line 75) | function addHistory (id, text, time, tags, pinned, notehistory) {
  function removeHistory (line 89) | function removeHistory (id, notehistory) {
  function writeHistory (line 100) | function writeHistory (title, tags) {
  function writeHistoryToStorage (line 112) | function writeHistoryToStorage (title, tags) {
  function renderHistory (line 129) | function renderHistory (title, tags) {
  function generateHistory (line 140) | function generateHistory (title, tags, notehistory) {
  function getHistory (line 157) | function getHistory (callback) {
  function getServerHistory (line 168) | function getServerHistory (callback) {
  function getStorageHistory (line 180) | function getStorageHistory (callback) {
  function parseHistory (line 190) | function parseHistory (list, callback) {
  function parseServerToHistory (line 201) | function parseServerToHistory (list, callback) {
  function parseStorageToHistory (line 213) | function parseStorageToHistory (list, callback) {
  function parseToHistory (line 222) | function parseToHistory (list, notehistory, callback) {
  function postHistoryToServer (line 251) | function postHistoryToServer (noteId, data, callback) {
  function deleteServerHistory (line 260) | function deleteServerHistory (noteId, callback) {

FILE: public/js/index.js
  function setHaveUnreadChanges (line 354) | function setHaveUnreadChanges (bool) {
  function updateTitleReminder (line 363) | function updateTitleReminder () {
  function setRefreshModal (line 372) | function setRefreshModal (status) {
  function setNeedRefresh (line 378) | function setNeedRefresh () {
  function autoSyncscroll (line 492) | function autoSyncscroll () {
  function windowResizeInner (line 503) | function windowResizeInner (callback) {
  function checkLayout (line 536) | function checkLayout () {
  function editorHasFocus (line 541) | function editorHasFocus () {
  function checkResponsive (line 546) | function checkResponsive () {
  function checkEditorStyle (line 562) | function checkEditorStyle () {
  function checkSyncToggle (line 639) | function checkSyncToggle () {
  function checkEditorScrollbarInner (line 658) | function checkEditorScrollbarInner () {
  function checkEditorScrollOverLines (line 666) | function checkEditorScrollOverLines () {
  function checkTocStyle (line 675) | function checkTocStyle () {
  function showStatus (line 712) | function showStatus (type, num) {
  function toggleMode (line 733) | function toggleMode () {
  function changeMode (line 749) | function changeMode (type) {
  function lockNavbar (line 868) | function lockNavbar () {
  function showMessageModal (line 876) | function showMessageModal (title, header, href, text, success) {
  function checkRevisionViewer (line 1093) | function checkRevisionViewer () {
  function parseRevisions (line 1102) | function parseRevisions (_revisions) {
  function selectRevision (line 1131) | function selectRevision (time) {
  function initRevisionViewer (line 1203) | function initRevisionViewer () {
  function scrollToTop (line 1269) | function scrollToTop () {
  function scrollToBottom (line 1283) | function scrollToBottom () {
  function generateScrollspy (line 1305) | function generateScrollspy () {
  function updateScrollspy (line 1325) | function updateScrollspy () {
  function applyScrollspyActive (line 1338) | function applyScrollspyActive (top, headerMap, headers, target, offset) {
  function parseToEditor (line 1493) | function parseToEditor (data) {
  function replaceAll (line 1503) | function replaceAll (data) {
  function importFromUrl (line 1513) | function importFromUrl (url) {
  function toggleNightMode (line 1589) | function toggleNightMode () {
  function emitPermission (line 1607) | function emitPermission (_permission) {
  function updatePermission (line 1613) | function updatePermission (newPermission) {
  function havePermission (line 1648) | function havePermission () {
  function updateInfo (line 1771) | function updateInfo (data) {
  function initMark (line 1803) | function initMark () {
  function initMarkAndCheckGutter (line 1812) | function initMarkAndCheckGutter (mark, author, timestamp) {
  function updateAuthorshipInner (line 1834) | function updateAuthorshipInner () {
  function iterateLine (line 1948) | function iterateLine (line) {
  function havePendingOperation (line 2048) | function havePendingOperation () {
  function updateOnlineStatus (line 2193) | function updateOnlineStatus () {
  function sortOnlineUserList (line 2240) | function sortOnlineUserList (list) {
  function renderUserStatusList (line 2269) | function renderUserStatusList (list) {
  function deduplicateOnlineUsers (line 2289) | function deduplicateOnlineUsers (list) {
  function emitUserStatus (line 2318) | function emitUserStatus (force) {
  function checkCursorTag (line 2343) | function checkCursorTag (coord, ele) {
  function buildCursor (line 2381) | function buildCursor (user) {
  function removeNullByte (line 2503) | function removeNullByte (cm, change) {
  function enforceMaxLength (line 2511) | function enforceMaxLength (cm, change) {
  function cursorActivityInner (line 2607) | function cursorActivityInner (editor) {
  function saveInfo (line 2665) | function saveInfo () {
  function restoreInfo (line 2693) | function restoreInfo () {
  function refreshView (line 2731) | function refreshView () {
  function updateViewInner (line 2744) | function updateViewInner () {
  function updateHistoryInner (line 2806) | function updateHistoryInner () {
  function updateDataAttrs (line 2810) | function updateDataAttrs (src, des) {
  function partialUpdate (line 2818) | function partialUpdate (src, tar, des) {
  function cloneAndRemoveDataAttr (line 2948) | function cloneAndRemoveDataAttr (el) {
  function copyAttribute (line 2956) | function copyAttribute (src, des, attr) {
  function reverseSortCursorMenu (line 2964) | function reverseSortCursorMenu (dropdown) {
  function checkCursorMenuInner (line 2974) | function checkCursorMenuInner () {
  function checkInIndentCode (line 3036) | function checkInIndentCode () {
  function checkInCode (line 3045) | function checkInCode () {
  function checkAbove (line 3049) | function checkAbove (method) {
  function checkBelow (line 3060) | function checkBelow (method) {
  function matchInCode (line 3072) | function matchInCode (text) {
  function checkInContainer (line 3090) | function checkInContainer () {
  function checkInContainerSyntax (line 3094) | function checkInContainerSyntax () {
  function matchInContainer (line 3100) | function matchInContainer (text) {

FILE: public/js/lib/common/login.js
  function setloginStateChangeEvent (line 12) | function setloginStateChangeEvent (func) {
  function resetCheckAuth (line 16) | function resetCheckAuth () {
  function setLoginState (line 20) | function setLoginState (bool, id) {
  function checkLoginStateChanged (line 36) | function checkLoginStateChanged () {
  function getLoginState (line 45) | function getLoginState () {
  function getUserId (line 50) | function getUserId () {
  function clearLoginState (line 54) | function clearLoginState () {
  function checkIfAuth (line 58) | function checkIfAuth (yesCallback, noCallback) {

FILE: public/js/lib/config/index.js
  constant DROPBOX_APP_KEY (line 1) | const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || ''

FILE: public/js/lib/editor/index.js
  class Storage (line 16) | class Storage {
    method get (line 17) | static get (key, defaultValue = null) {
    method set (line 27) | static set (key, value, options = {}) {
    method remove (line 37) | static remove (key) {
  class Editor (line 55) | class Editor {
    method constructor (line 56) | constructor () {
    method migratePreferences (line 226) | migratePreferences () {
    method on (line 255) | on (event, cb) {
    method addToolBar (line 267) | addToolBar () {
    method addStatusBar (line 428) | addStatusBar () {
    method updateStatusBar (line 455) | updateStatusBar () {
    method handleStatusBarResize (line 477) | handleStatusBarResize () {
    method setIndent (line 492) | setIndent () {
    method setKeymap (line 586) | setKeymap () {
    method setTheme (line 620) | setTheme () {
    method setSpellcheckLang (line 652) | setSpellcheckLang (lang) {
    method getExistingSpellcheckLang (line 670) | getExistingSpellcheckLang () {
    method activateSpellcheckListItem (line 680) | activateSpellcheckListItem (lang) {
    method setSpellcheck (line 690) | setSpellcheck () {
    method toggleLinter (line 749) | toggleLinter (enable) {
    method setLinter (line 765) | setLinter () {
    method resetEditorKeymapToBrowserKeymap (line 783) | resetEditorKeymapToBrowserKeymap () {
    method restoreOverrideEditorKeymap (line 791) | restoreOverrideEditorKeymap () {
    method setOverrideBrowserKeymap (line 799) | setOverrideBrowserKeymap () {
    method setPreferences (line 812) | setPreferences () {
    method setTableShortcutsPreference (line 849) | setTableShortcutsPreference () {
    method init (line 864) | init (textit) {
    method getEditor (line 906) | getEditor () {

FILE: public/js/lib/editor/markdown-lint/index.js
  function validator (line 16) | function validator (text) {
  method onClick (line 55) | onClick () {
  method onClick (line 88) | onClick () {
  function lint (line 99) | function lint (content) {
  function normalizeFixInfo (line 117) | function normalizeFixInfo (fixInfo, lineNumber) {

FILE: public/js/lib/editor/spellcheck.js
  function request (line 73) | function request (url) {
  function runSeriesP (line 86) | async function runSeriesP (iterables, fn) {
  function mapSeriesP (line 94) | function mapSeriesP (iterables, fn) {
  function createTypo (line 100) | function createTypo (lang, affData, dicData) {
  function findOrCreateTypoInstance (line 107) | async function findOrCreateTypoInstance (lang) {
  class CodeMirrorSpellChecker (line 155) | class CodeMirrorSpellChecker {
    method constructor (line 160) | constructor (cm, lang, editor) {
    method setDictLang (line 174) | setDictLang (lang) {
    method defineSpellCheckerMode (line 184) | defineSpellCheckerMode (cm, lang) {

FILE: public/js/lib/editor/table-editor.js
  class TextEditorInterface (line 8) | class TextEditorInterface {
    method constructor (line 9) | constructor (editor) {
    method getCursorPosition (line 16) | getCursorPosition () {
    method setCursorPosition (line 21) | setCursorPosition (pos) {
    method setSelectionRange (line 25) | setSelectionRange (range) {
    method getLastRow (line 32) | getLastRow () {
    method acceptsTableEdit (line 36) | acceptsTableEdit () {
    method getLine (line 40) | getLine (row) {
    method insertLine (line 44) | insertLine (row, line) {
    method deleteLine (line 62) | deleteLine (row) {
    method replaceLines (line 90) | replaceLines (startRow, endRow, lines) {
    method transact (line 108) | transact (func) {
  function initTableEditor (line 118) | function initTableEditor (editor) {

FILE: public/js/lib/editor/utils.js
  function wrapTextWith (line 3) | function wrapTextWith (editor, cm, symbol) {
  function insertText (line 51) | function insertText (cm, text, cursorEnd = 0) {
  function insertLink (line 58) | function insertLink (cm, isImage) {
  function insertHeader (line 90) | function insertHeader (cm) {
  function insertOnStartOfLines (line 103) | function insertOnStartOfLines (cm, symbol) {

FILE: public/js/lib/markdown/utils.js
  function parseFenceCodeParams (line 1) | function parseFenceCodeParams (lang) {
  function serializeParamToAttribute (line 35) | function serializeParamToAttribute (params) {
  function deserializeParamAttributeFromElement (line 46) | function deserializeParamAttributeFromElement (elem) {

FILE: public/js/lib/renderer/csvpreview.js
  function renderCSVPreview (line 12) | function renderCSVPreview (csv, options = {}, attr = '') {

FILE: public/js/lib/renderer/lightbox/index.js
  function findOrCreateLightboxContainer (line 11) | function findOrCreateLightboxContainer () {
  function switchImage (line 59) | function switchImage (dir) {
  function setImageInner (line 74) | function setImageInner (img, lightBoxContainer) {
  function onClickImage (line 82) | function onClickImage (img) {
  function updateLightboxImages (line 94) | function updateLightboxImages () {
  function addImageZoomListener (line 102) | function addImageZoomListener (container) {
  function addImageDragListener (line 132) | function addImageDragListener (image) {

FILE: public/js/lib/syncscroll.js
  function addPart (line 13) | function addPart (tokens, idx) {
  function renderContainer (line 120) | function renderContainer (tokens, idx, options, env, self) {
  function setupSyncAreas (line 174) | function setupSyncAreas (edit, view, markdown, _editor) {
  function clearMap (line 187) | function clearMap () {
  function buildMapInner (line 200) | function buildMapInner (callback) {
  function syncScrollToEdit (line 278) | function syncScrollToEdit (event, preventAnimate) {
  function viewScrollingTimeoutInner (line 353) | function viewScrollingTimeoutInner () {
  function syncScrollToView (line 360) | function syncScrollToView (event, preventAnimate) {
  function editScrollingTimeoutInner (line 413) | function editScrollingTimeoutInner () {

FILE: public/js/pretty.js
  function generateScrollspy (line 86) | function generateScrollspy () {
  function windowResize (line 101) | function windowResize () {
  function scrollToTop (line 134) | function scrollToTop () {
  function scrollToBottom (line 140) | function scrollToBottom () {

FILE: public/js/render.js
  function preventXSS (line 70) | function preventXSS (html) {

FILE: public/js/reveal-markdown.js
  function getMarkdownFromSlide (line 32) | function getMarkdownFromSlide (section) {
  function getForwardedAttributes (line 59) | function getForwardedAttributes (section) {
  function getSlidifyOptions (line 84) | function getSlidifyOptions (options) {
  function createMarkdownSlide (line 96) | function createMarkdownSlide (content, options) {
  function slidify (line 116) | function slidify (markdown, options) {
  function processSlides (line 183) | function processSlides () {
  function addAttributeInElement (line 250) | function addAttributeInElement (node, elementTarget, separator) {
  function addAttributes (line 274) | function addAttributes (section, element, previousElement, separatorElem...
  function convertSlides (line 312) | function convertSlides () {

FILE: public/js/revealjs-plugins/elapsed-time-bar/elapsed-time-bar.js
  method handleReady (line 19) | handleReady() {
  method loop (line 79) | loop() {
  method setBarColor (line 95) | setBarColor() {
  method start (line 108) | start(allottedTime, elapsedTime = 0) {
  method reset (line 118) | reset() {
  method pause (line 122) | pause() {
  method resume (line 129) | resume() {

FILE: public/js/revealjs-plugins/spotlight/spotlight.js
  function onRevealJsReady (line 23) | function onRevealJsReady(event) {
  function configure (line 43) | function configure() {
  function setupCanvas (line 103) | function setupCanvas() {
  function addWindowResizeListener (line 128) | function addWindowResizeListener() {
  function addMouseMoveListener (line 136) | function addMouseMoveListener() {
  function addMouseToggleSpotlightListener (line 145) | function addMouseToggleSpotlightListener() {
  function addKeyPressAndHoldSpotlightListener (line 160) | function addKeyPressAndHoldSpotlightListener(keyCode) {
  function toggleSpotlight (line 175) | function toggleSpotlight() {
  function setSpotlight (line 179) | function setSpotlight(isOn, mouseEvt) {
  function togglePresentationMode (line 199) | function togglePresentationMode() {
  function setCursor (line 203) | function setCursor(isOn) {
  function showSpotlight (line 218) | function showSpotlight(mouseEvt) {
  function getMousePosByMovement (line 246) | function getMousePosByMovement(canvas, evt) {
  function getMousePosByBoundingClientRect (line 271) | function getMousePosByBoundingClientRect(canvas, evt) {

FILE: public/js/slide.js
  function extend (line 26) | function extend () {
  method condition (line 43) | condition () {
  method condition (line 49) | condition () {
  function renderSlide (line 128) | function renderSlide (event) {

FILE: public/js/utils.js
  function checkNoteIdValid (line 6) | function checkNoteIdValid (id) {
  function encodeNoteId (line 11) | function encodeNoteId (id) {
  function decodeNoteId (line 18) | function decodeNoteId (encodedId) {
  function sanitizeUrl (line 38) | function sanitizeUrl (rawUrl) {
  function isPdfUrl (line 52) | async function isPdfUrl (url) {

FILE: public/vendor/abcjs_basic_3.1.1-min.js
  function calcHorizontalSpacing (line 2) | function calcHorizontalSpacing(isLastLine,stretchLast,targetWidth,lineWi...
  function centerWholeRests (line 2) | function centerWholeRests(voices){for(var i=0;i<voices.length;i++)for(va...
  function kernSymbols (line 2) | function kernSymbols(lastSymbol,thisSymbol,lastSymbolWidth){var width=la...
  function R (line 2) | function R(first){if(R.is(first,"function"))return loaded?first():eve.on...
  function clone (line 2) | function clone(obj){if("function"==typeof obj||Object(obj)!==obj)return ...
  function repush (line 2) | function repush(array,item){for(var i=0,ii=array.length;i<ii;i++)if(arra...
  function cacher (line 2) | function cacher(f,scope,postprocessor){function newf(){var arg=Array.pro...
  function clrToString (line 2) | function clrToString(){return this.hex}
  function catmullRom2bezier (line 2) | function catmullRom2bezier(crp,z){for(var d=[],i=0,iLen=crp.length;iLen-...
  function base3 (line 2) | function base3(t,p1,p2,p3,p4){var t1=-3*p1+9*p2-9*p3+3*p4,t2=t*t1+6*p1-1...
  function bezlen (line 2) | function bezlen(x1,y1,x2,y2,x3,y3,x4,y4,z){null==z&&(z=1),z=z>1?1:z<0?0:...
  function getTatLen (line 2) | function getTatLen(x1,y1,x2,y2,x3,y3,x4,y4,ll){if(!(ll<0||bezlen(x1,y1,x...
  function intersect (line 2) | function intersect(x1,y1,x2,y2,x3,y3,x4,y4){if(!(mmax(x1,x2)<mmin(x3,x4)...
  function interHelper (line 2) | function interHelper(bez1,bez2,justCount){var bbox1=R.bezierBBox(bez1),b...
  function interPathHelper (line 2) | function interPathHelper(path1,path2,justCount){path1=R._path2curve(path...
  function Matrix (line 2) | function Matrix(a,b,c,d,e,f){null!=a?(this.a=+a,this.b=+b,this.c=+c,this...
  function x_y_w_h (line 2) | function x_y_w_h(){return this.x+S+this.y+S+this.width+" × "+this.height}
  function CubicBezierAtTime (line 2) | function CubicBezierAtTime(t,p1x,p1y,p2x,p2y,duration){function sampleCu...
  function Animation (line 2) | function Animation(anim,ms){var percents=[],newAnim={};if(this.ms=ms,thi...
  function runAnimation (line 2) | function runAnimation(anim,element,percent,status,totalOrigin,times){per...
  function stopAnimation (line 2) | function stopAnimation(paper){for(var i=0;i<animationElements.length;i++...
  function norm (line 3) | function norm(a){return a[0]*a[0]+a[1]*a[1]}
  function normalize (line 3) | function normalize(a){var mag=math.sqrt(norm(a));a[0]&&(a[0]/=mag),a[1]&...
  function start (line 3) | function start(e){(e.originalEvent||e).preventDefault();var x=e.clientX,...
  function isLoaded (line 4) | function isLoaded(){/in/.test(doc.readyState)?setTimeout(isLoaded,9):R.e...
  function hasClass (line 5) | function hasClass(element,cls){var elClass=element.getAttribute("class")...
  function getAllElementsByClasses (line 5) | function getAllElementsByClasses(startingEl,class1,class2){for(var els=s...
  function getCssRule (line 5) | function getCssRule(selector){for(var rule,i=0;i<document.styleSheets.le...
  function getBeatsPerMinute (line 5) | function getBeatsPerMinute(tune,options){var bpm;return bpm=options.bpm?...
  function setMargin (line 5) | function setMargin(margin){cssRule.style.marginTop=-margin+"px",currentM...
  function scrolling (line 5) | function scrolling(){var currentPosition=paper.style.marginLeft;currentP...
  function processMeasureHider (line 5) | function processMeasureHider(lineNum,measureNum){var els=getAllElementsB...
  function addVerticalInfo (line 5) | function addVerticalInfo(timingEvents){for(var lastBarTop,lastBarBottom,...
  function makeSortedArray (line 5) | function makeSortedArray(hash){var arr=[];for(var k in hash)hash.hasOwnP...
  function setupEvents (line 5) | function setupEvents(engraver){for(var eventHash={},time=0,isTiedState=!...
  function isEndOfLine (line 5) | function isEndOfLine(currentNote){return currentNote.top!==currentNote.n...
  function shouldScroll (line 5) | function shouldScroll(outer,scrollPos,currentNote){var height=parseInt(o...
  function processShowCursor (line 5) | function processShowCursor(){var currentNote=timingEvents.shift();return...
  function processNext (line 5) | function processNext(){if(stopNextTime)return void ABCJS.stopAnimation()...
  function renderEngine (line 5) | function renderEngine(callback,output,abc,parserParams,renderParams){var...
  function resizeOuter (line 5) | function resizeOuter(){var width=window.innerWidth;for(var id in resizeD...
  function renderOne (line 5) | function renderOne(div,tune,renderParams,engraverParams){var width=rende...
  function renderEachLineSeparately (line 5) | function renderEachLineSeparately(div,tune,renderParams,engraverParams){...
  function callback (line 6) | function callback(div,tune){!renderParams.oneSvgPerLine||tune.lines.leng...
  function callback (line 6) | function callback(div,tune,index){var html="",midi=window.ABCJS.midi.cre...
  function addVerticalInfo (line 6) | function addVerticalInfo(timingEvents){for(var lastBarTop,lastBarBottom,...
  function makeSortedArray (line 6) | function makeSortedArray(hash){var arr=[];for(var k in hash)hash.hasOwnP...
  function cleanUpSlursInLine (line 6) | function cleanUpSlursInLine(line){for(var x,addEndSlur=function(obj,num,...
  function fixClefPlacement (line 6) | function fixClefPlacement(el){window.ABCJS.parse.parseKeyVoice.fixClef(el)}
  function getNextMusicLine (line 6) | function getNextMusicLine(lines,currentLine){for(currentLine++;lines.len...
  function isFunction (line 6) | function isFunction(functionToCheck){var getType={};return functionToChe...
  function preprocessLabel (line 6) | function preprocessLabel(label,title){return label.replace(/%T/g,title)}
  function hasClass (line 6) | function hasClass(element,cls){return!!element&&(" "+element.className+"...
  function addClass (line 6) | function addClass(element,cls){element&&(hasClass(element,cls)||(element...
  function removeClass (line 6) | function removeClass(element,cls){element&&(element.className=element.cl...
  function toggleClass (line 6) | function toggleClass(element,cls){element&&(hasClass(element,cls)?remove...
  function closest (line 6) | function closest(element,cls){if(!element)return null;for(;element!==doc...
  function find (line 6) | function find(element,cls){if(!element)return null;var els=element.getEl...
  function addLoadEvent (line 6) | function addLoadEvent(func){var oldOnLoad=window.onload;"function"!=type...
  function afterSetup (line 6) | function afterSetup(timeWarp,data,onSuccess){MIDI.player.currentTime=0,M...
  function setCurrentMidiTune (line 6) | function setCurrentMidiTune(timeWarp,data,onSuccess){midiJsInitialized?a...
  function startCurrentlySelectedTune (line 6) | function startCurrentlySelectedTune(){MIDI.player.start(MIDI.player.curr...
  function stopCurrentlyPlayingTune (line 6) | function stopCurrentlyPlayingTune(){MIDI.player.stop()}
  function pauseCurrentlyPlayingTune (line 6) | function pauseCurrentlyPlayingTune(){MIDI.player.pause()}
  function setMidiCallback (line 6) | function setMidiCallback(midiJsListener){MIDI.player.setAnimation(midiJs...
  function jumpToMidiPosition (line 6) | function jumpToMidiPosition(play,offset,width){var ratio=offset/width,en...
  function setTimeWarp (line 6) | function setTimeWarp(percent){MIDI.player.warp=percent>0?100/percent:1}
  function loadMidi (line 6) | function loadMidi(target,onSuccess){var dataEl=find(target,"abcjs-data")...
  function deselectMidiControl (line 6) | function deselectMidiControl(){var otherMidi=find(document,"abcjs-midi-c...
  function findElements (line 6) | function findElements(visualItems,currentTime,epsilon){for(var currentIn...
  function midiJsListener (line 6) | function midiJsListener(position){var midiControl;if(position.duration>0...
  function onStart (line 6) | function onStart(target){var parent=closest(target,"abcjs-inline-midi");...
  function onSelection (line 6) | function onSelection(target){toggleClass(target,"abcjs-pushed")}
  function onLoop (line 6) | function onLoop(target){toggleClass(target,"abcjs-pushed")}
  function doReset (line 6) | function doReset(target,callback){function onSuccess(){addClass(parent,"...
  function onReset (line 6) | function onReset(target){function keepPlaying(){play&&(startCurrentlySel...
  function relMouseX (line 6) | function relMouseX(target,event){var totalOffsetX=0;do totalOffsetX+=tar...
  function onProgress (line 6) | function onProgress(target,event){var parent=closest(target,"abcjs-inlin...
  function onTempo (line 6) | function onTempo(el){for(var percent=parseInt(el.value,10),startTempo=pa...
  function addDelegates (line 6) | function addDelegates(){if(document.body.addEventListener("click",functi...
  function convertPitch (line 7) | function convertPitch(pitch){return 60+pitch}
  function findChord (line 7) | function findChord(elem){if(chordTrackFinished||!elem.chord||0===elem.ch...
  function timeFromStart (line 7) | function timeFromStart(){for(var distance=0,ct=0;ct<currentTrack.length;...
  function writeNote (line 7) | function writeNote(elem,voiceOff){var velocity=voiceOff?0:64,chord=findC...
  function adjustPitch (line 7) | function adjustPitch(note){var pitch=note.pitch;if(note.accidental)switc...
  function setKeySignature (line 7) | function setKeySignature(elem){var accidentals=[0,0,0,0,0,0,0];if(!elem....
  function processGraceNotes (line 7) | function processGraceNotes(graces,companionDuration){for(var grace,grace...
  function writeGraceNotes (line 7) | function writeGraceNotes(graces,stealFromCurrent,duration,skipNote,veloc...
  function extractOctave (line 7) | function extractOctave(pitch){return Math.floor(pitch/7)}
  function extractNote (line 7) | function extractNote(pitch){return pitch%=7,pitch<0&&(pitch+=7),pitch}
  function interpretChord (line 7) | function interpretChord(name){if(0!==name.length){if("break"===name)retu...
  function chordNotes (line 7) | function chordNotes(bass,modifier){var intervals=chordIntervals[modifier...
  function writeBoom (line 7) | function writeBoom(boom,beatLength){void 0!==boom&&chordTrack.push({cmd:...
  function writeChick (line 7) | function writeChick(chick,beatLength){for(var c=0;c<chick.length;c++)cho...
  function resolveChords (line 7) | function resolveChords(){var num=meter.num,den=meter.den,beatLength=1/de...
  function normalizeDrumDefinition (line 7) | function normalizeDrumDefinition(params){if(0===params.pattern.length||p...
  function drumBeat (line 7) | function drumBeat(pitch,soundLength,volume){drumTrack.push({cmd:"start",...
  function writeDrum (line 7) | function writeDrum(){if(0!==drumTrack.length||drumDefinition.on){var mea...
  function addAbsoluteTime (line 7) | function addAbsoluteTime(tracks){for(var i=0;i<tracks.length;i++)for(var...
  function combineTracks (line 7) | function combineTracks(tracks){for(var output=[],i=0;i<tracks.length;i++...
  function sortTracks (line 7) | function sortTracks(output){return output.sort(function(a,b){return a.ab...
  function adjustTime (line 7) | function adjustTime(output){for(var lastTime=0,i=0;i<output.length;i++){...
  function weaveTracks (line 7) | function weaveTracks(tracks){addAbsoluteTime(tracks);var output=combineT...
  function setAttributes (line 7) | function setAttributes(elm,attrs){for(var attr in attrs)attrs.hasOwnProp...
  function Midi (line 7) | function Midi(){this.trackstrings="",this.trackcount=0,this.noteOnAndCha...
  function encodeHex (line 7) | function encodeHex(s){for(var ret="",i=0;i<s.length;i+=2)ret+="%",ret+=s...
  function toHex (line 7) | function toHex(n,padding){for(var s=n.toString(16);s.length<padding;)s="...
  function toDurationHex (line 7) | function toDurationHex(n){for(var res=0,a=[];0!==n;)a.push(127&n),n>>=7;...
  function interpretTempo (line 7) | function interpretTempo(element){var duration=.25;element.duration&&(dur...
  function interpretMeter (line 7) | function interpretMeter(element){var meter;switch(element.type){case"com...
  function addPositioning (line 7) | function addPositioning(el,type,value){el.positioning||(el.positioning={...
  function addFont (line 7) | function addFont(el,type,value){el.fonts||(el.fonts={}),el.fonts[type]=v...
  function startNewLine (line 7) | function startNewLine(){var params={startChar:-1,endChar:-1};if(multilin...
  function durationOfMeasure (line 8) | function durationOfMeasure(multilineVars){var meter=multilineVars.origMe...
  function appendLastMeasure (line 8) | function appendLastMeasure(voice,nextVoice){voice.push({el_type:"hint"})...
  function addHintMeasure (line 8) | function addHintMeasure(staff,nextStaff){for(var i=0;i<staff.length;i++)...
  function addHintMeasures (line 8) | function addHintMeasures(){for(var i=0;i<tune.lines.length;i++){var line...
  function initializeFonts (line 8) | function initializeFonts(){multilineVars.annotationfont={face:"Helvetica...
  function processNumberOnly (line 9) | function processNumberOnly(){var size=parseInt(tokens[0].token);return t...
  function minStem (line 12) | function minStem(element,stemsUp,referencePitch,minStemHeight){if(!eleme...
  function calcSlant (line 12) | function calcSlant(leftAveragePitch,rightAveragePitch,numStems,isFlat){i...
  function calcAverage (line 12) | function calcAverage(total,numElements){return numElements?total/numElem...
  function getBarYAt (line 12) | function getBarYAt(startx,starty,endx,endy,x){return starty+(endy-starty...
  function calcDy (line 12) | function calcDy(asc,isGrace){var dy=asc?ABCJS.write.spacing.STEP:-ABCJS....
  function drawBeam (line 12) | function drawBeam(renderer,startX,startY,endX,endY,dy,isHint){var klass=...
  function calcXPos (line 12) | function calcXPos(asc,firstElement,lastElement){var starthead=firstEleme...
  function calcYPos (line 12) | function calcYPos(total,numElements,stemHeight,asc,firstAveragePitch,las...
  function createStems (line 12) | function createStems(elems,asc,beam,dy,mainNote){for(var i=0;i<elems.len...
  function createAdditionalBeams (line 12) | function createAdditionalBeams(elems,asc,beam,isGrace,dy){for(var beams=...
  function highestPitch (line 12) | function highestPitch(){if(0===abselem.heads.length)return 10;for(var pi...
  function lowestPitch (line 12) | function lowestPitch(){if(0===abselem.heads.length)return 2;for(var pitc...
  function compoundDecoration (line 12) | function compoundDecoration(symbol,count){var placement="down"===dir?low...
  function incrementPlacement (line 12) | function incrementPlacement(placement,height){"above"===placement?yPos.a...
  function getPlacement (line 12) | function getPlacement(placement){var y;return"above"===placement?(y=yPos...
  function textDecoration (line 12) | function textDecoration(text,placement){var y=getPlacement(placement),te...
  function symbolDecoration (line 12) | function symbolDecoration(symbol,placement){var deltaX=width/2;"center"!...
  function setPaddingVariable (line 16) | function setPaddingVariable(self,paddingKey,formattingKey,printDefault,s...
  function getPitch (line 17) | function getPitch(anchor,isAbove,isTie){return isTie?anchor.pitch:isAbov...
  function drawLine (line 17) | function drawLine(renderer,l,t,r,b){var pathString=ABCJS.write.sprintf("...
  function drawBracket (line 17) | function drawBracket(renderer,x1,y1,x2,y2){y1=renderer.calcY(y1),y2=rend...

FILE: public/vendor/jquery-textcomplete/jquery.textcomplete.js
  function Completer (line 120) | function Completer(element, option) {
  function Dropdown (line 340) | function Dropdown(element, completer, option) {
  function Strategy (line 753) | function Strategy(options) {
  function Adapter (line 819) | function Adapter () {}
  function Textarea (line 945) | function Textarea(element, completer, option) {
  function IETextarea (line 1072) | function IETextarea(element, completer, option) {
  function ContentEditable (line 1126) | function ContentEditable (element, completer, option) {

FILE: public/vendor/md-toc.js
  function Toc (line 10) | function Toc (id, options) {

FILE: public/vendor/ot/ajax-adapter.js
  function AjaxAdapter (line 6) | function AjaxAdapter (path, ownUserName, revision) {

FILE: public/vendor/ot/client.js
  function Client (line 11) | function Client (revision) {
  function Synchronized (line 65) | function Synchronized () {}
  function AwaitingConfirm (line 99) | function AwaitingConfirm (outstanding) {
  function AwaitingWithBuffer (line 154) | function AwaitingWithBuffer (outstanding, buffer) {
  function Stale (line 218) | function Stale(acknowlaged, client, revision) {
  function StaleWithBuffer (line 259) | function StaleWithBuffer(acknowlaged, buffer, client, revision) {

FILE: public/vendor/ot/codemirror-adapter.js
  function CodeMirrorAdapter (line 9) | function CodeMirrorAdapter(cm) {
  function cmpPos (line 37) | function cmpPos(a, b) {
  function posEq (line 53) | function posEq(a, b) {
  function posLe (line 57) | function posLe(a, b) {
  function minPos (line 61) | function minPos(a, b) {
  function maxPos (line 65) | function maxPos(a, b) {
  function codemirrorDocLength (line 69) | function codemirrorDocLength(doc) {
  function last (line 101) | function last(arr) {
  function sumLengths (line 105) | function sumLengths(strArr) {
  function updateIndexFromPos (line 116) | function updateIndexFromPos(indexFromPos, change) {
  function assert (line 361) | function assert(b, msg) {
  function bind (line 370) | function bind(obj, method) {
  function hex2rgb (line 381) | function hex2rgb(hex) {

FILE: public/vendor/ot/editor-client.js
  function SelfMeta (line 13) | function SelfMeta (selectionBefore, selectionAfter) {
  function OtherMeta (line 34) | function OtherMeta (clientId, selection) {
  function OtherClient (line 54) | function OtherClient (id, listEl, editorAdapter, name, color, selection) {
  function EditorClient (line 123) | function EditorClient (revision, clients, serverAdapter, editorAdapter) {
  function rgb2hex (line 305) | function rgb2hex (r, g, b) {
  function hsl2hex (line 313) | function hsl2hex (h, s, l) {
  function hueFromName (line 328) | function hueFromName (name) {
  function inherit (line 337) | function inherit (Const, Super) {
  function last (line 344) | function last (arr) { return arr[arr.length - 1]; }
  function removeElement (line 347) | function removeElement (el) {

FILE: public/vendor/ot/selection.js
  function Range (line 15) | function Range (anchor, head) {
  function transformIndex (line 33) | function transformIndex (index) {
  function Selection (line 60) | function Selection (ranges) {

FILE: public/vendor/ot/socketio-adapter.js
  function SocketIOAdapter (line 6) | function SocketIOAdapter(socket) {

FILE: public/vendor/ot/text-operation.js
  function TextOperation (line 10) | function TextOperation () {
  function getSimpleOp (line 337) | function getSimpleOp (operation, fn) {
  function getStartIndex (line 351) | function getStartIndex (operation) {

FILE: public/vendor/ot/undo-manager.js
  function UndoManager (line 14) | function UndoManager (maxItems) {
  function transformStack (line 47) | function transformStack (stack, operation) {

FILE: public/vendor/ot/wrapped-operation.js
  function WrappedOperation (line 10) | function WrappedOperation (operation, meta) {
  function copy (line 29) | function copy (source, target) {
  function composeMeta (line 37) | function composeMeta (a, b) {
  function transformMeta (line 55) | function transformMeta (meta, operation) {

FILE: public/vendor/showup/showup.js
  function checkScrollTop (line 34) | function checkScrollTop()

FILE: test/realtime/utils.js
  function makeMockSocket (line 6) | function makeMockSocket (headers, query) {
  function removeModuleFromRequireCache (line 36) | function removeModuleFromRequireCache (modulePath) {
  function removeLibModuleCache (line 39) | function removeLibModuleCache () {

FILE: test/testDoubles/ProcessQueueFake.js
  class ProcessQueueFake (line 3) | class ProcessQueueFake {
    method constructor (line 4) | constructor () {
    method start (line 9) | start () {
    method stop (line 13) | stop () {
    method checkTaskIsInQueue (line 17) | checkTaskIsInQueue (id) {
    method push (line 21) | push (id, processFunc) {
    method process (line 29) | process () {

FILE: test/testDoubles/loggerFake.js
  function createFakeLogger (line 5) | function createFakeLogger () {

FILE: test/testDoubles/otFake.js
  class EditorSocketIOServerFake (line 5) | class EditorSocketIOServerFake {
    method constructor (line 6) | constructor () {

FILE: test/testDoubles/realtimeJobStub.js
  class realtimeJobStub (line 3) | class realtimeJobStub {
    method start (line 4) | start () {
    method stop (line 7) | stop () {

FILE: utils/string.js
  function stripTags (line 3) | function stripTags (s) {
Condensed preview — 284 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,491K chars).
[
  {
    "path": ".buildpacks",
    "chars": 87,
    "preview": "https://github.com/Scalingo/apt-buildpack\nhttps://github.com/Scalingo/nodejs-buildpack\n"
  },
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 661,
    "preview": "# [Choice] Node.js version: 16, 14\nARG VARIANT=14-bullseye\nFROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 1603,
    "preview": "{\n\t\"name\": \"CodiMD\",\n\t\"dockerComposeFile\": \"docker-compose.yml\",\n\t\"service\": \"app\",\n\t\"workspaceFolder\": \"/workspace\",\n\n\t"
  },
  {
    "path": ".devcontainer/docker-compose.yml",
    "chars": 1426,
    "preview": "version: '3'\n\nservices: \n  app:\n    build:\n      context: ..\n      dockerfile: .devcontainer/Dockerfile\n      args:\n    "
  },
  {
    "path": ".dockerignore",
    "chars": 156,
    "preview": ".idea\ncoverage\nnode_modules/\n\n# ignore config files\nconfig.json\n.sequelizerc\n\n# ignore webpack build\npublic/build\npublic"
  },
  {
    "path": ".editorconfig",
    "chars": 329,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[{*.h"
  },
  {
    "path": ".github/tests/README.md",
    "chars": 202,
    "preview": "# Test github actions with act\n\n```bash\nact pull_request --container-architecture linux/arm64 -e .github/tests/pull-requ"
  },
  {
    "path": ".github/tests/pull-request.json",
    "chars": 120,
    "preview": "{\n  \"pull_request\": {\n    \"head\": {\n      \"ref\": \"release/1.2.3\"\n    },\n    \"base\": {\n      \"ref\": \"master\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1332,
    "preview": "name: 'Test and Build'\n\non:\n  push:\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  test-and-build:\n    runs-on: ubuntu-la"
  },
  {
    "path": ".github/workflows/check-release.yml",
    "chars": 1124,
    "preview": "name: Release PR Checks\n\non:\n  workflow_dispatch:\n  pull_request:\n    branches:\n      - master\n\njobs:\n  check-release-pr"
  },
  {
    "path": ".github/workflows/push-image.yml",
    "chars": 3553,
    "preview": "name: Build and push image\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      runtime:\n      "
  },
  {
    "path": ".gitignore",
    "chars": 365,
    "preview": "node_modules\ncomposer.phar\ncomposer.lock\n.env.*.php\n.env.php\n.DS_Store\n.idea/\nThumbs.db\nnpm-debug.log\nhackmd_io\nnewrelic"
  },
  {
    "path": ".mailmap",
    "chars": 867,
    "preview": "Max Wu <jackymaxj@gmail.com>                 Wu Cheng-Han <jacky_cute0808@hotmail.com>\nMax Wu <jackymaxj@gmail.com>     "
  },
  {
    "path": ".nvmrc",
    "chars": 9,
    "preview": "v16.20.2\n"
  },
  {
    "path": ".sequelizerc.example",
    "chars": 251,
    "preview": "const path = require('path')\nconst config = require('./lib/config')\n\nmodule.exports = {\n  config: path.resolve('config.j"
  },
  {
    "path": "AUTHORS",
    "chars": 2563,
    "preview": "alecdwm <alec@owls.io>\nbananaappletw <bananaappletw@gmail.com>\nBartlomiej Szala <fenix440@gmail.com>\nBoHong Li <a60814bi"
  },
  {
    "path": "Aptfile",
    "chars": 12,
    "preview": "libvips-dev\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4054,
    "preview": "# Contributing\n\nWhen contributing to this repository, please first discuss the change you wish to make via issue,\nemail,"
  },
  {
    "path": "FUNDING.json",
    "chars": 107,
    "preview": "{\n  \"drips\": {\n    \"ethereum\": {\n      \"ownedBy\": \"0xEd37B84FD84A834886aC07693aF6A9cd35040002\"\n    }\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 34519,
    "preview": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C)"
  },
  {
    "path": "Procfile",
    "chars": 27,
    "preview": "web: ./bin/heroku_start.sh\n"
  },
  {
    "path": "README.md",
    "chars": 6789,
    "preview": "CodiMD\n===\n\n[![build status][build-image]][build-url]\n[![version][github-version-badge]][github-release-page]\n[![Gitter]"
  },
  {
    "path": "app.js",
    "chars": 9744,
    "preview": "'use strict'\n// app\n// external modules\nvar express = require('express')\n\nvar ejs = require('ejs')\nvar passport = requir"
  },
  {
    "path": "app.json",
    "chars": 5807,
    "preview": "{\n    \"name\": \"CodiMD\",\n    \"description\": \"Realtime collaborative markdown notes on all platforms\",\n    \"keywords\": [\n "
  },
  {
    "path": "babel.config.js",
    "chars": 376,
    "preview": "module.exports = {\n  presets: [\n    ['@babel/preset-env', {\n      targets: {\n        node: '14'\n      },\n      useBuiltI"
  },
  {
    "path": "bin/heroku",
    "chars": 175,
    "preview": "#!/bin/bash\n\nset -e\n\nif [ ! -z \"$DYNO\" ]; then\n  # setup config files\n  cp .sequelizerc.example .sequelizerc\n\n    cat <<"
  },
  {
    "path": "bin/heroku_start.sh",
    "chars": 90,
    "preview": "#!/bin/bash\n\nset -euo pipefail\n\nCMD_DB_URL=\"$DATABASE_URL\" CMD_PORT=\"$PORT\" npm run start\n"
  },
  {
    "path": "bin/manage_users",
    "chars": 3583,
    "preview": "#!/usr/bin/env node\n\n// First configure the logger so it does not spam the console\nconst logger = require('../lib/logger"
  },
  {
    "path": "bin/setup",
    "chars": 827,
    "preview": "#!/bin/bash\n\nset -e\n\n# run command at repo root\nCURRENT_PATH=$PWD\nif [ -d .git ]; then\n  cd \"$(git rev-parse --show-topl"
  },
  {
    "path": "config.js",
    "chars": 67,
    "preview": "const config = require('./lib/config')\n\nmodule.exports = config.db\n"
  },
  {
    "path": "config.json.example",
    "chars": 4861,
    "preview": "{\n    \"test\": {\n        \"db\": {\n            \"dialect\": \"sqlite\",\n            \"storage\": \":memory:\"\n        },\n        \"l"
  },
  {
    "path": "contribute/developer-certificate-of-origin",
    "chars": 1421,
    "preview": "Developer Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n660 Yor"
  },
  {
    "path": "deployments/Dockerfile",
    "chars": 935,
    "preview": "ARG RUNTIME\nARG BUILDPACK\n\nFROM $BUILDPACK as BUILD\n\nCOPY --chown=hackmd:hackmd . .\nENV QT_QPA_PLATFORM=offscreen\n\nRUN s"
  },
  {
    "path": "deployments/build.sh",
    "chars": 527,
    "preview": "#!/usr/bin/env bash\n\nset -eo pipefail\nset -x\n\nif [[ -z $1 || -z $2 ]];then\n    echo \"build.sh [runtime image] [buildpack"
  },
  {
    "path": "deployments/docker-compose.yml",
    "chars": 1000,
    "preview": "version: \"3\"\nservices:\n  database:\n    image: postgres:11.6-alpine\n    environment:\n      - POSTGRES_USER=codimd\n      -"
  },
  {
    "path": "deployments/docker-entrypoint.sh",
    "chars": 344,
    "preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nif [[ \"$#\" -gt 0 ]]; then\n    exec \"$@\"\n    exit $?\nfi\n\n# check database and red"
  },
  {
    "path": "lib/auth/bitbucket/index.js",
    "chars": 932,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst BitbucketStrategy = re"
  },
  {
    "path": "lib/auth/dropbox/index.js",
    "chars": 937,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst DropboxStrategy = requ"
  },
  {
    "path": "lib/auth/email/index.js",
    "chars": 2748,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst sequelize = require('s"
  },
  {
    "path": "lib/auth/facebook/index.js",
    "chars": 912,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst FacebookStrategy = req"
  },
  {
    "path": "lib/auth/github/index.js",
    "chars": 2459,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst request = require('request')\nconst passport = require('pass"
  },
  {
    "path": "lib/auth/gitlab/index.js",
    "chars": 1426,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst GitlabStrategy = requi"
  },
  {
    "path": "lib/auth/google/index.js",
    "chars": 1031,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nvar GoogleStrategy = require"
  },
  {
    "path": "lib/auth/index.js",
    "chars": 1927,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\n\nconst config = require('../"
  },
  {
    "path": "lib/auth/ldap/index.js",
    "chars": 2957,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst LDAPStrategy = require"
  },
  {
    "path": "lib/auth/mattermost/index.js",
    "chars": 1501,
    "preview": "'use strict'\nrequire('babel-polyfill')\nrequire('isomorphic-fetch')\nconst Router = require('express').Router\nconst passpo"
  },
  {
    "path": "lib/auth/oauth2/index.js",
    "chars": 1067,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\n\nconst config = require('../"
  },
  {
    "path": "lib/auth/oauth2/strategy.js",
    "chars": 2613,
    "preview": "'use strict'\n\nconst { Strategy, InternalOAuthError } = require('passport-oauth2')\nconst config = require('../../config')"
  },
  {
    "path": "lib/auth/openid/index.js",
    "chars": 1781,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst OpenIDStrategy = requi"
  },
  {
    "path": "lib/auth/saml/index.js",
    "chars": 3317,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst SamlStrategy = require"
  },
  {
    "path": "lib/auth/twitter/index.js",
    "chars": 908,
    "preview": "'use strict'\n\nconst Router = require('express').Router\nconst passport = require('passport')\nconst TwitterStrategy = requ"
  },
  {
    "path": "lib/auth/utils.js",
    "chars": 2034,
    "preview": "'use strict'\n\nconst models = require('../models')\nconst config = require('../config')\nconst logger = require('../logger'"
  },
  {
    "path": "lib/config/default.js",
    "chars": 5039,
    "preview": "'use strict'\n\nconst os = require('os')\n\nmodule.exports = {\n  domain: '',\n  urlPath: '',\n  host: '0.0.0.0',\n  port: 3000,"
  },
  {
    "path": "lib/config/defaultSSL.js",
    "chars": 404,
    "preview": "'use strict'\n\nconst fs = require('fs')\n\nfunction getFile (path) {\n  if (fs.existsSync(path)) {\n    return path\n  }\n  ret"
  },
  {
    "path": "lib/config/dockerSecret.js",
    "chars": 2205,
    "preview": "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\n\nconst basePath = path.resolve('/var/run/secrets/')\n"
  },
  {
    "path": "lib/config/enum.js",
    "chars": 272,
    "preview": "'use strict'\n\nexports.Environment = {\n  development: 'development',\n  production: 'production',\n  test: 'test'\n}\n\nexport"
  },
  {
    "path": "lib/config/environment.js",
    "chars": 6800,
    "preview": "'use strict'\n\nconst { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')\n\nmodule.exports = {\n  sourc"
  },
  {
    "path": "lib/config/index.js",
    "chars": 8270,
    "preview": "\n'use strict'\n\nconst crypto = require('crypto')\nconst fs = require('fs')\nconst path = require('path')\nconst { merge } = "
  },
  {
    "path": "lib/config/utils.js",
    "chars": 1705,
    "preview": "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\n\nexports.toBooleanConfig = function toBooleanConfig "
  },
  {
    "path": "lib/csp.js",
    "chars": 3684,
    "preview": "var config = require('./config')\nvar uuid = require('uuid')\n\nvar CspStrategy = {}\n\nvar defaultDirectives = {\n  defaultSr"
  },
  {
    "path": "lib/errorPage/index.js",
    "chars": 547,
    "preview": "'use strict'\n\nconst config = require('../config')\nconst { responseError } = require('../response')\n\nexports.errorForbidd"
  },
  {
    "path": "lib/history/index.js",
    "chars": 6122,
    "preview": "'use strict'\n// history\n// external modules\nvar LZString = require('@hackmd/lz-string')\n\n// core\nvar config = require('."
  },
  {
    "path": "lib/homepage/index.js",
    "chars": 972,
    "preview": "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\nconst config = require('../config')\nconst { User } ="
  },
  {
    "path": "lib/imageRouter/azure.js",
    "chars": 1105,
    "preview": "'use strict'\nconst path = require('path')\n\nconst config = require('../config')\nconst logger = require('../logger')\n\ncons"
  },
  {
    "path": "lib/imageRouter/filesystem.js",
    "chars": 1614,
    "preview": "'use strict'\n\nconst crypto = require('crypto')\nconst fs = require('fs')\nconst URL = require('url').URL\nconst path = requ"
  },
  {
    "path": "lib/imageRouter/imgur.js",
    "chars": 791,
    "preview": "'use strict'\nconst config = require('../config')\nconst logger = require('../logger')\n\nconst imgur = require('@hackmd/img"
  },
  {
    "path": "lib/imageRouter/index.js",
    "chars": 1921,
    "preview": "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\nconst Router = require('express').Router\nconst formi"
  },
  {
    "path": "lib/imageRouter/lutim.js",
    "chars": 811,
    "preview": "'use strict'\nconst config = require('../config')\nconst logger = require('../logger')\n\nconst lutim = require('lutim')\n\nex"
  },
  {
    "path": "lib/imageRouter/minio.js",
    "chars": 1419,
    "preview": "'use strict'\nconst fs = require('fs')\nconst path = require('path')\n\nconst config = require('../config')\nconst { getImage"
  },
  {
    "path": "lib/imageRouter/s3.js",
    "chars": 1976,
    "preview": "'use strict'\nconst fs = require('fs')\nconst path = require('path')\n\nconst config = require('../config')\nconst { getImage"
  },
  {
    "path": "lib/letter-avatars.js",
    "chars": 1548,
    "preview": "'use strict'\n// external modules\nconst crypto = require('crypto')\nconst randomcolor = require('randomcolor')\nconst confi"
  },
  {
    "path": "lib/logger.js",
    "chars": 549,
    "preview": "'use strict'\nconst { createLogger, format, transports } = require('winston')\n\nconst logger = createLogger({\n  level: 'de"
  },
  {
    "path": "lib/metrics.js",
    "chars": 355,
    "preview": "'use strict'\n\nconst { Router } = require('express')\n\nconst { wrap } = require('./utils')\n\n// load controller\nconst statu"
  },
  {
    "path": "lib/middleware/checkURIValid.js",
    "chars": 278,
    "preview": "'use strict'\n\nconst logger = require('../logger')\nconst response = require('../response')\n\nmodule.exports = function (re"
  },
  {
    "path": "lib/middleware/codiMDVersion.js",
    "chars": 168,
    "preview": "'use strict'\n\nconst config = require('../config')\n\nmodule.exports = function (req, res, next) {\n  res.set({\n    'CodiMD-"
  },
  {
    "path": "lib/middleware/redirectWithoutTrailingSlashes.js",
    "chars": 498,
    "preview": "'use strict'\n\nconst config = require('../config')\n\nmodule.exports = function (req, res, next) {\n  if (req.method === 'GE"
  },
  {
    "path": "lib/middleware/tooBusy.js",
    "chars": 308,
    "preview": "'use strict'\n\nconst toobusy = require('toobusy-js')\n\nconst config = require('../config')\nconst response = require('../re"
  },
  {
    "path": "lib/migrations/20150504155329-create-users.js",
    "chars": 561,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.createTable('User"
  },
  {
    "path": "lib/migrations/20150508114741-create-notes.js",
    "chars": 511,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.createTable('Note"
  },
  {
    "path": "lib/migrations/20150515125813-create-temp.js",
    "chars": 406,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.createTable('Temp"
  },
  {
    "path": "lib/migrations/20150702001020-update-to-0_3_1.js",
    "chars": 1476,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20150915153700-change-notes-title-to-text.js",
    "chars": 768,
    "preview": "'use strict'\nconst isSQLite = require('../utils').isSQLite\nmodule.exports = {\n  up: function (queryInterface, Sequelize)"
  },
  {
    "path": "lib/migrations/20160112220142-note-add-lastchange.js",
    "chars": 925,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20160420180355-note-add-alias.js",
    "chars": 850,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20160515114000-user-add-tokens.js",
    "chars": 849,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Users'"
  },
  {
    "path": "lib/migrations/20160607060246-support-revision.js",
    "chars": 1115,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20160703062241-support-authorship.js",
    "chars": 1291,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20161009040430-support-delete-note.js",
    "chars": 638,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Notes'"
  },
  {
    "path": "lib/migrations/20161201050312-support-email-signin.js",
    "chars": 1114,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Users'"
  },
  {
    "path": "lib/migrations/20171009121200-longtext-for-mysql.js",
    "chars": 880,
    "preview": "'use strict'\nmodule.exports = {\n  up: async function (queryInterface, Sequelize) {\n    await queryInterface.changeColumn"
  },
  {
    "path": "lib/migrations/20180209120907-longtext-of-authorship.js",
    "chars": 521,
    "preview": "'use strict'\n\nmodule.exports = {\n  up: async function (queryInterface, Sequelize) {\n    await queryInterface.changeColum"
  },
  {
    "path": "lib/migrations/20180306150303-fix-enum.js",
    "chars": 433,
    "preview": "'use strict'\n\nmodule.exports = {\n  up: async function (queryInterface, Sequelize) {\n    await queryInterface.changeColum"
  },
  {
    "path": "lib/migrations/20180326103000-use-text-in-tokens.js",
    "chars": 603,
    "preview": "'use strict'\n\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.changeColumn('Us"
  },
  {
    "path": "lib/migrations/20180525153000-user-add-delete-token.js",
    "chars": 333,
    "preview": "'use strict'\nmodule.exports = {\n  up: function (queryInterface, Sequelize) {\n    return queryInterface.addColumn('Users'"
  },
  {
    "path": "lib/migrations/20200104215332-remove-temp-table.js",
    "chars": 595,
    "preview": "'use strict'\n\nmodule.exports = {\n  up: (queryInterface, Sequelize) => {\n    return queryInterface.dropTable('Temp')\n    "
  },
  {
    "path": "lib/migrations/20240114120250-revision-add-index.js",
    "chars": 250,
    "preview": "'use strict'\n\nmodule.exports = {\n  up: (queryInterface, Sequelize) => {\n    return queryInterface.addIndex('Revisions', "
  },
  {
    "path": "lib/models/author.js",
    "chars": 805,
    "preview": "'use strict'\n// external modules\nvar Sequelize = require('sequelize')\n\nmodule.exports = function (sequelize, DataTypes) "
  },
  {
    "path": "lib/models/index.js",
    "chars": 1523,
    "preview": "'use strict'\n// external modules\nvar fs = require('fs')\nvar path = require('path')\nvar Sequelize = require('sequelize')\n"
  },
  {
    "path": "lib/models/note.js",
    "chars": 20414,
    "preview": "'use strict'\n// external modules\nvar fs = require('fs')\nvar path = require('path')\nvar LZString = require('@hackmd/lz-st"
  },
  {
    "path": "lib/models/revision.js",
    "chars": 9561,
    "preview": "'use strict'\n// external modules\nvar Sequelize = require('sequelize')\nvar async = require('async')\nvar moment = require("
  },
  {
    "path": "lib/models/user.js",
    "chars": 4800,
    "preview": "'use strict'\n// external modules\nvar Sequelize = require('sequelize')\nvar Scrypt = require('scrypt-kdf')\n\n// core\nvar lo"
  },
  {
    "path": "lib/note/index.js",
    "chars": 9107,
    "preview": "'use strict'\n\nconst config = require('../config')\nconst logger = require('../logger')\nconst { Note, User, Revision } = r"
  },
  {
    "path": "lib/note/noteActions.js",
    "chars": 7632,
    "preview": "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\nconst markdownpdf = require('markdown-pdf')\nconst sh"
  },
  {
    "path": "lib/ot/client.js",
    "chars": 10810,
    "preview": "// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.sca"
  },
  {
    "path": "lib/ot/editor-socketio-server.js",
    "chars": 5392,
    "preview": "'use strict';\n\nvar EventEmitter = require('events').EventEmitter;\nvar TextOperation = require('./text-operation');\nvar W"
  },
  {
    "path": "lib/ot/index.js",
    "chars": 383,
    "preview": "exports.version = '0.0.15';\n\nexports.TextOperation        = require('./text-operation');\nexports.SimpleTextOperation  = "
  },
  {
    "path": "lib/ot/selection.js",
    "chars": 3499,
    "preview": "if (typeof ot === 'undefined') {\n  // Export for browsers\n  var ot = {};\n}\n\not.Selection = (function (global) {\n  'use s"
  },
  {
    "path": "lib/ot/server.js",
    "chars": 1675,
    "preview": "var config = require('../config');\n\nif (typeof ot === 'undefined') {\n  var ot = {};\n}\n\not.Server = (function (global) {\n"
  },
  {
    "path": "lib/ot/simple-text-operation.js",
    "chars": 6055,
    "preview": "if (typeof ot === 'undefined') {\n  // Export for browsers\n  var ot = {};\n}\n\not.SimpleTextOperation = (function (global) "
  },
  {
    "path": "lib/ot/text-operation.js",
    "chars": 17604,
    "preview": "if (typeof ot === 'undefined') {\n  // Export for browsers\n  var ot = {};\n}\n\not.TextOperation = (function () {\n  'use str"
  },
  {
    "path": "lib/ot/wrapped-operation.js",
    "chars": 2057,
    "preview": "if (typeof ot === 'undefined') {\n  // Export for browsers\n  var ot = {};\n}\n\not.WrappedOperation = (function (global) {\n "
  },
  {
    "path": "lib/realtime/processQueue.js",
    "chars": 2264,
    "preview": "'use strict'\n\nconst EventEmitter = require('events').EventEmitter\n\n/**\n * Queuing Class for connection queuing\n */\n\ncons"
  },
  {
    "path": "lib/realtime/realtime.js",
    "chars": 22981,
    "preview": "'use strict'\n// realtime\n// external modules\nconst cookie = require('cookie')\nconst cookieParser = require('cookie-parse"
  },
  {
    "path": "lib/realtime/realtimeCleanDanglingUserJob.js",
    "chars": 1296,
    "preview": "'use strict'\n\nconst async = require('async')\nconst config = require('../config')\nconst logger = require('../logger')\n\n/*"
  },
  {
    "path": "lib/realtime/realtimeClientConnection.js",
    "chars": 7356,
    "preview": "'use strict'\n\nconst get = require('lodash/get')\n\nconst config = require('../config')\nconst models = require('../models')"
  },
  {
    "path": "lib/realtime/realtimeSaveRevisionJob.js",
    "chars": 924,
    "preview": "'use strict'\n\nconst models = require('../models')\nconst logger = require('../logger')\n\n/**\n * clean when user not in any"
  },
  {
    "path": "lib/realtime/realtimeUpdateDirtyNoteJob.js",
    "chars": 1975,
    "preview": "'use strict'\n\nconst config = require('../config')\nconst logger = require('../logger')\nconst moment = require('moment')\n\n"
  },
  {
    "path": "lib/response.js",
    "chars": 11884,
    "preview": "'use strict'\n// response\n// external modules\nconst request = require('request')\n\n// core\nconst config = require('./confi"
  },
  {
    "path": "lib/routes.js",
    "chars": 3063,
    "preview": "'use strict'\n\nconst { Router } = require('express')\n\nconst { wrap, urlencodedParser, markdownParser } = require('./utils"
  },
  {
    "path": "lib/status/index.js",
    "chars": 1518,
    "preview": "'use strict'\n\nconst realtime = require('../realtime/realtime')\nconst config = require('../config')\n\nexports.getStatus = "
  },
  {
    "path": "lib/user/index.js",
    "chars": 2959,
    "preview": "'use strict'\n\nconst archiver = require('archiver')\nconst async = require('async')\n\nconst response = require('../response"
  },
  {
    "path": "lib/utils.js",
    "chars": 1007,
    "preview": "'use strict'\nconst fs = require('fs')\nconst path = require('path')\nconst bodyParser = require('body-parser')\nconst mime "
  },
  {
    "path": "lib/web/middleware/checkVersion.js",
    "chars": 1637,
    "preview": "'use strict'\n\nconst { promisify } = require('util')\n\nconst request = require('request')\n\nconst logger = require('../../l"
  },
  {
    "path": "lib/workers/dmpWorker.js",
    "chars": 4101,
    "preview": "'use strict'\n// external modules\nvar DiffMatchPatch = require('@hackmd/diff-match-patch')\nvar dmp = new DiffMatchPatch()"
  },
  {
    "path": "locales/ca.json",
    "chars": 4342,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Notes col·laboratives a Markdown\",\n\t\"Realtime collaborative markdown notes on all pl"
  },
  {
    "path": "locales/da.json",
    "chars": 4174,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Kollaborative markdown-noter\",\n\t\"Realtime collaborative markdown notes on all platfo"
  },
  {
    "path": "locales/de.json",
    "chars": 5515,
    "preview": "{\n    \"Collaborative markdown notes\": \"Gemeinsame Markdown Notizen\",\n    \"Realtime collaborative markdown notes on all p"
  },
  {
    "path": "locales/el.json",
    "chars": 4462,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Συνεργατικές σημειώσεις markdown\",\n\t\"Realtime collaborative markdown notes on all pl"
  },
  {
    "path": "locales/en.json",
    "chars": 4972,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Collaborative markdown notes\",\n\t\"Realtime collaborative markdown notes on all platfo"
  },
  {
    "path": "locales/eo.json",
    "chars": 4191,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Kunlaborataj marksubenaj notoj\",\n\t\"Realtime collaborative markdown notes on all plat"
  },
  {
    "path": "locales/es.json",
    "chars": 4355,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Notas colaborativas en Markdown\",\n\t\"Realtime collaborative markdown notes on all pla"
  },
  {
    "path": "locales/fr.json",
    "chars": 5857,
    "preview": "{\n    \"Collaborative markdown notes\": \"Notes collaboratives en markdown\",\n    \"Realtime collaborative markdown notes on "
  },
  {
    "path": "locales/hi.json",
    "chars": 4232,
    "preview": "{\n\t\"Collaborative markdown notes\": \"सहयोगात्मक मार्कडॉऊन नोट्स\",\n\t\"Realtime collaborative markdown notes on all platform"
  },
  {
    "path": "locales/hr.json",
    "chars": 4327,
    "preview": "{\r\n\t\"Collaborative markdown notes\": \"Kolaborativne markdown bilješke\",\r\n\t\"Realtime collaborative markdown notes on all p"
  },
  {
    "path": "locales/id.json",
    "chars": 5485,
    "preview": "{\n    \"Collaborative markdown notes\": \"Catatan markdown kolaboratif\",\n    \"Realtime collaborative markdown notes on all "
  },
  {
    "path": "locales/it.json",
    "chars": 5570,
    "preview": "{\n    \"Collaborative markdown notes\": \"Note collaborative in markdown\",\n    \"Realtime collaborative markdown notes on al"
  },
  {
    "path": "locales/ja.json",
    "chars": 4429,
    "preview": "{\n    \"Collaborative markdown notes\": \"共同編集できるMarkdownノート\",\n    \"Realtime collaborative markdown notes on all platforms."
  },
  {
    "path": "locales/ko.json",
    "chars": 4423,
    "preview": "{\n    \"Collaborative markdown notes\": \"협동 마크다운 노트\",\n    \"Realtime collaborative markdown notes on all platforms.\": \"실시간으"
  },
  {
    "path": "locales/nl.json",
    "chars": 5627,
    "preview": "{\n    \"Collaborative markdown notes\": \"Samenwerkende markdown notities\",\n    \"Realtime collaborative markdown notes on a"
  },
  {
    "path": "locales/pl.json",
    "chars": 5339,
    "preview": "{\n    \"Collaborative markdown notes\": \"Wspólne markdown notatki\",\n    \"Realtime collaborative markdown notes on all plat"
  },
  {
    "path": "locales/pt.json",
    "chars": 4298,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Notas em Markdown colaborativas\",\n\t\"Realtime collaborative markdown notes on all pla"
  },
  {
    "path": "locales/ru.json",
    "chars": 5612,
    "preview": "{\n    \"Collaborative markdown notes\": \"Совместные markdown заметки\",\n    \"Realtime collaborative markdown notes on all p"
  },
  {
    "path": "locales/sr.json",
    "chars": 5536,
    "preview": "{\n    \"Collaborative markdown notes\": \"Дељене белешке у Markdown формату\",\n    \"Realtime collaborative markdown notes on"
  },
  {
    "path": "locales/sv.json",
    "chars": 4183,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Kollaborativa markdownanteckningar\",\n\t\"Realtime collaborative markdown notes on all "
  },
  {
    "path": "locales/tr.json",
    "chars": 4180,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Ortak markdown notları\",\n\t\"Realtime collaborative markdown notes on all platforms.\":"
  },
  {
    "path": "locales/uk.json",
    "chars": 4298,
    "preview": "{\n\t\"Collaborative markdown notes\": \"Спільні примітки щодо знижок\",\n\t\"Realtime collaborative markdown notes on all platfo"
  },
  {
    "path": "locales/zh-CN.json",
    "chars": 4805,
    "preview": "{\n    \"Collaborative markdown notes\": \"Markdown 协作笔记\",\n    \"Realtime collaborative markdown notes on all platforms.\": \"使"
  },
  {
    "path": "locales/zh-TW.json",
    "chars": 4821,
    "preview": "{\n    \"Collaborative markdown notes\": \"Markdown 協作筆記\",\n    \"Realtime collaborative markdown notes on all platforms.\": \"使"
  },
  {
    "path": "package.json",
    "chars": 7810,
    "preview": "{\n  \"name\": \"codimd\",\n  \"version\": \"2.6.0\",\n  \"description\": \"Realtime collaborative markdown notes on all platforms.\",\n"
  },
  {
    "path": "public/.eslintrc.js",
    "chars": 702,
    "preview": "// this config file is used in concert with the root .eslintrc.js in the root dir.\nmodule.exports = {\n  \"env\": {\n    \"br"
  },
  {
    "path": "public/css/bootstrap-social.css",
    "chars": 20328,
    "preview": "/*\n * Social Buttons for Bootstrap\n *\n * Copyright 2013-2014 Panayiotis Lipiridis\n * Licensed under the MIT License\n *\n "
  },
  {
    "path": "public/css/center.css",
    "chars": 196,
    "preview": "html,\nbody,\n.container-fluid {\n    height: 98%;\n}\n.container-fluid {\n    display: table;\n    vertical-align: middle;\n}\n."
  },
  {
    "path": "public/css/codemirror-extend/ayu-dark.css",
    "chars": 2243,
    "preview": "/* Based on https://github.com/dempfi/ayu */\n\n.cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; }\n.cm-s-a"
  },
  {
    "path": "public/css/codemirror-extend/ayu-mirage.css",
    "chars": 2357,
    "preview": "/* Based on https://github.com/dempfi/ayu */\n\n.cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; }\n.cm-s"
  },
  {
    "path": "public/css/codemirror-extend/one-dark.css",
    "chars": 217,
    "preview": ".cm-s-one-dark .CodeMirror-linenumber {\n  color: #676767;\n}\n\n.cm-s-one-dark.CodeMirror-focused\n  .CodeMirror-activeline\n"
  },
  {
    "path": "public/css/codemirror-extend/tomorrow-night-bright.css",
    "chars": 1769,
    "preview": "/*\n\n    Name:       Tomorrow Night - Bright\n    Author:     Chris Kempson\n\n    Port done by Gerard Braad <me@gbraad.nl>\n"
  },
  {
    "path": "public/css/codemirror-extend/tomorrow-night-eighties.css",
    "chars": 2439,
    "preview": "/*\n\n    Name:       Tomorrow Night - Eighties\n    Author:     Chris Kempson\n\n    CodeMirror template by Jan T. Sott (htt"
  },
  {
    "path": "public/css/cover.css",
    "chars": 7745,
    "preview": "/*\n * Globals\n */\n\n/* Links */\n\na,\na:focus,\na:hover {\n    color: #fff;\n}\n/* Custom default button */\n\n.btn-default,\n.btn"
  },
  {
    "path": "public/css/extra.css",
    "chars": 11569,
    "preview": "/* for extra features should include this */\n\n.vimeo,\n.youtube {\n    position: relative;\n    cursor: pointer;\n    displa"
  },
  {
    "path": "public/css/font.css",
    "chars": 8862,
    "preview": "/* latin-ext */\n@font-face {\n  font-family: 'Source Code Pro';\n  font-style: normal;\n  font-weight: 300;\n  src: local('S"
  },
  {
    "path": "public/css/github-extract.css",
    "chars": 9916,
    "preview": ".markdown-body {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \""
  },
  {
    "path": "public/css/google-font.css",
    "chars": 180,
    "preview": "@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,300italic,300|Source+Ser"
  },
  {
    "path": "public/css/index.css",
    "chars": 13243,
    "preview": "html, body {\n    height: 100%;\n}\nbody {\n    margin: 0;\n    padding: 0;\n    max-width: inherit;\n    min-width: 200px;\n   "
  },
  {
    "path": "public/css/markdown.css",
    "chars": 4452,
    "preview": "/* for markdown-body */\n\n.markdown-body {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helve"
  },
  {
    "path": "public/css/mermaid.css",
    "chars": 5043,
    "preview": "/* Flowchart variables */\n/* Sequence Diagram variables */\n/* Gantt chart variables */\n.mermaid .label {\n  color: #333;\n"
  },
  {
    "path": "public/css/site.css",
    "chars": 731,
    "preview": "/* for all pages should include this */\nbody {\n    font-smoothing: subpixel-antialiased !important;\n    -webkit-font-smo"
  },
  {
    "path": "public/css/slide-preview.css",
    "chars": 1077,
    "preview": ".markdown-body.slides {\n  position: relative;\n  z-index: 1;\n  color: #222;\n}\n\n.markdown-body.slides::before {\n  content:"
  },
  {
    "path": "public/css/slide.css",
    "chars": 5556,
    "preview": ".reveal {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Helvetica, Arial, sa"
  },
  {
    "path": "public/default.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "public/docs/features.md",
    "chars": 17811,
    "preview": "Features\n===\n\nIntroduction\n===\n<i class=\"fa fa-file-text\"></i> **CodiMD** is a real-time, multi-platform collaborative m"
  },
  {
    "path": "public/docs/privacy.md.example",
    "chars": 1972,
    "preview": "Privacy\n===\n\nWe process the following data, for the following purposes:\n\n|your data|our usage|\n|---------|---------|\n|IP"
  },
  {
    "path": "public/docs/release-notes.md",
    "chars": 60261,
    "preview": "Release Notes\n===\n\n<i class=\"fa fa-tag\"></i> 2.6.0 <i class=\"fa fa-clock-o\"></i> 2025-06-10\n---\n\n[Check out the complete"
  },
  {
    "path": "public/docs/slide-example.md",
    "chars": 5813,
    "preview": "---\ntype: slide\nslideOptions:\n  transition: slide\n---\n\n# Slide example\n\nThis feature still in beta, may have some issues"
  },
  {
    "path": "public/docs/yaml-metadata.md",
    "chars": 3394,
    "preview": "---\nrobots: index, follow\nlang: en\ndir: ltr\nbreaks: true\n---\n\nSupported YAML metadata\n===\n\nFirst you need to insert synt"
  },
  {
    "path": "public/js/cover.js",
    "chars": 11639,
    "preview": "/* eslint-env browser, jquery */\n/* global moment, serverurl */\n\nimport {\n  checkIfAuth,\n  clearLoginState,\n  getLoginSt"
  },
  {
    "path": "public/js/extra.js",
    "chars": 50196,
    "preview": "/* eslint-env browser, jquery */\n/* global moment, serverurl, plantumlServer, L */\n\nimport Prism from 'prismjs'\nimport h"
  },
  {
    "path": "public/js/history.js",
    "chars": 7267,
    "preview": "/* eslint-env browser, jquery */\n/* global serverurl, moment */\n\nimport store from 'store'\nimport LZString from '@hackmd"
  },
  {
    "path": "public/js/htmlExport.js",
    "chars": 193,
    "preview": "require('../css/github-extract.css')\nrequire('../css/markdown.css')\nrequire('../css/extra.css')\nrequire('../css/slide-pr"
  },
  {
    "path": "public/js/index.js",
    "chars": 103122,
    "preview": "/* eslint-env browser, jquery */\n/* global CodeMirror, Cookies, moment, serverurl,\n   key, Dropbox, ot, hex2rgb, Visibil"
  },
  {
    "path": "public/js/lib/appState.js",
    "chars": 144,
    "preview": "import modeType from './modeType'\n\nconst state = {\n  syncscroll: true,\n  currentMode: modeType.view,\n  nightMode: false\n"
  },
  {
    "path": "public/js/lib/common/constant.ejs",
    "chars": 501,
    "preview": "window.domain = '<%- domain %>'\nwindow.urlpath = '<%- urlpath %>'\nwindow.debug = <%- debug %>\nwindow.version = '<%- vers"
  },
  {
    "path": "public/js/lib/common/login.js",
    "chars": 1954,
    "preview": "/* eslint-env browser, jquery */\n/* global Cookies */\n\nimport { serverurl } from '../config'\n\nlet checkAuth = false\nlet "
  },
  {
    "path": "public/js/lib/common/metrics.ejs",
    "chars": 1158,
    "preview": "# HELP online_notes Number of currently used notes\n# TYPE online_notes gauge\nonline_notes <%- onlineNotes %>\n# HELP onli"
  },
  {
    "path": "public/js/lib/config/index.js",
    "chars": 753,
    "preview": "export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || ''\n\nexport const domain = window.domain || '' // domain name\nex"
  },
  {
    "path": "public/js/lib/editor/config.js",
    "chars": 63,
    "preview": "const config = {\n  docmaxlength: null\n}\n\nexport default config\n"
  },
  {
    "path": "public/js/lib/editor/constants.js",
    "chars": 888,
    "preview": "import { serverurl } from '../config'\n\nexport const availableThemes = [\n  { name: 'Light', value: 'default' },\n  { name:"
  },
  {
    "path": "public/js/lib/editor/index.js",
    "chars": 28076,
    "preview": "/* eslint-env browser */\n/* global CodeMirror, $, editor, Cookies */\nimport { options, Alignment, FormatType } from '@su"
  },
  {
    "path": "public/js/lib/editor/markdown-lint/index.js",
    "chars": 3652,
    "preview": "/* global CodeMirror */\n\n// load CM lint plugin explicitly\nimport '@hackmd/codemirror/addon/lint/lint'\n\nimport '@hackmd/"
  },
  {
    "path": "public/js/lib/editor/spellcheck.js",
    "chars": 5451,
    "preview": "/* eslint-env browser */\n\n// Modified from https://github.com/sparksuite/codemirror-spell-checker\n\nimport Typo from 'typ"
  },
  {
    "path": "public/js/lib/editor/statusbar.html",
    "chars": 3083,
    "preview": "<div class=\"status-bar\">\n    <div class=\"status-info\">\n        <div class=\"status-cursor\">\n          <span class=\"status"
  },
  {
    "path": "public/js/lib/editor/table-editor.js",
    "chars": 7274,
    "preview": "/* global CodeMirror, $ */\nimport { TableEditor, Point, options, Alignment, FormatType } from '@susisu/mte-kernel'\n\n// p"
  },
  {
    "path": "public/js/lib/editor/toolbar.html",
    "chars": 6281,
    "preview": "<div class=\"toolbar\">\n  <div class=\"btn-toolbar\" role=\"toolbar\" aria-label=\"Editor toolbar\">\n    <div class=\"btn-group\" "
  },
  {
    "path": "public/js/lib/editor/ui-elements.js",
    "chars": 2809,
    "preview": "/*\n *  Global UI elements references\n */\n/* global $ */\n\nexport const getUIElements = () => ({\n  spinner: $('.ui-spinner"
  },
  {
    "path": "public/js/lib/editor/utils.js",
    "chars": 4116,
    "preview": "/* global CodeMirror, editor */\nconst wrapSymbols = ['*', '_', '~', '^', '+', '=']\nexport function wrapTextWith (editor,"
  },
  {
    "path": "public/js/lib/markdown/utils.js",
    "chars": 1653,
    "preview": "export function parseFenceCodeParams (lang) {\n  const attrMatch = lang.match(/{(.*)}/)\n  const params = {}\n  if (attrMat"
  },
  {
    "path": "public/js/lib/modeType.js",
    "chars": 114,
    "preview": "export default {\n  edit: {\n    name: 'edit'\n  },\n  view: {\n    name: 'view'\n  },\n  both: {\n    name: 'both'\n  }\n}\n"
  },
  {
    "path": "public/js/lib/renderer/csvpreview.js",
    "chars": 1023,
    "preview": "import Papa from 'papaparse'\nimport escapeHTML from 'lodash/escape'\n\nconst safeParse = d => {\n  try {\n    return JSON.pa"
  },
  {
    "path": "public/js/lib/renderer/fretboard/css/i.css",
    "chars": 2614,
    "preview": "/* -- GENERAL TYPOGRAPHY -- */\n.fretTitle {\n  color: #555;\n  text-align: center;\n  font-family: \"Helvetica Neue\", sans-s"
  },
  {
    "path": "public/js/lib/renderer/fretboard/fretboard.js",
    "chars": 3221,
    "preview": "/* global $ */\nimport escapeHTML from 'lodash/escape'\n\nimport './css/i.css'\nimport dotEmpty from './svg/dotEmpty.svg'\nim"
  },
  {
    "path": "public/js/lib/renderer/lightbox/index.js",
    "chars": 5449,
    "preview": "import './lightbox.css'\nimport escape from 'lodash/escape'\n\nlet images = []\n/** @type {HTMLImageElement} */\nlet currentI"
  }
]

// ... and 84 more files (download for full content)

About this extraction

This page contains the full source code of the hackmdio/codimd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 284 files (2.2 MB), approximately 600.8k tokens, and a symbol index with 679 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!