├── public ├── default.md ├── uploads │ └── .gitkeep ├── views │ ├── codimd │ │ ├── footer.ejs │ │ └── head.ejs │ ├── index │ │ ├── footer.ejs │ │ ├── header.ejs │ │ ├── foot.ejs │ │ └── head.ejs │ ├── includes │ │ ├── header.ejs │ │ └── scripts.ejs │ ├── index.ejs │ ├── codimd.ejs │ ├── shared │ │ ├── ga.ejs │ │ ├── disqus.ejs │ │ ├── polyfill.ejs │ │ ├── revision-modal.ejs │ │ ├── refresh-modal.ejs │ │ └── pandoc-export-modal.ejs │ └── error.ejs ├── favicon.png ├── screenshot.png ├── apple-touch-icon.png ├── codimd-icon-1024.png ├── js │ ├── lib │ │ ├── editor │ │ │ ├── config.js │ │ │ └── constants.js │ │ ├── modeType.js │ │ ├── appState.js │ │ ├── renderer │ │ │ ├── fretboard │ │ │ │ └── svg │ │ │ │ │ ├── dotWideMiddle.svg │ │ │ │ │ ├── dotWideLeft.svg │ │ │ │ │ ├── dot.svg │ │ │ │ │ ├── dotWideRight.svg │ │ │ │ │ ├── dotEmpty.svg │ │ │ │ │ ├── dotEmpty_h.svg │ │ │ │ │ ├── dot_h.svg │ │ │ │ │ ├── string_o.svg │ │ │ │ │ ├── string_x.svg │ │ │ │ │ ├── fretb_vert_4.svg │ │ │ │ │ ├── fretb_vert_5.svg │ │ │ │ │ ├── fretb_horiz_5.svg │ │ │ │ │ ├── fretb_horiz_6.svg │ │ │ │ │ ├── fretb_horiz_7.svg │ │ │ │ │ ├── fretb_vert_7.svg │ │ │ │ │ ├── fretb_vert_9.svg │ │ │ │ │ ├── fretb_vert_12.svg │ │ │ │ │ ├── fretb_vert_15.svg │ │ │ │ │ ├── number7.svg │ │ │ │ │ ├── number7_h.svg │ │ │ │ │ ├── number1.svg │ │ │ │ │ ├── number1_h.svg │ │ │ │ │ ├── number4.svg │ │ │ │ │ └── number4_h.svg │ │ │ └── csvpreview.js │ │ ├── common │ │ │ ├── constant.ejs │ │ │ ├── metrics.ejs │ │ │ └── login.js │ │ ├── config │ │ │ └── index.js │ │ └── markdown │ │ │ └── utils.js │ ├── htmlExport.js │ ├── mathjax-config-extra.js │ ├── locale.js │ └── utils.js ├── fonts │ ├── SourceCodePro-Black.eot │ ├── SourceCodePro-Black.ttf │ ├── SourceCodePro-Bold.eot │ ├── SourceCodePro-Bold.ttf │ ├── SourceCodePro-Bold.woff │ ├── SourceCodePro-Light.eot │ ├── SourceCodePro-Light.ttf │ ├── SourceSansPro-Black.eot │ ├── SourceSansPro-Black.ttf │ ├── SourceSansPro-Bold.eot │ ├── SourceSansPro-Bold.ttf │ ├── SourceSansPro-Bold.woff │ ├── SourceSansPro-Light.eot │ ├── SourceSansPro-Light.ttf │ ├── SourceSerifPro-Bold.eot │ ├── SourceSerifPro-Bold.ttf │ ├── SourceCodePro-Black.woff │ ├── SourceCodePro-Light.woff │ ├── SourceCodePro-Medium.eot │ ├── SourceCodePro-Medium.ttf │ ├── SourceCodePro-Medium.woff │ ├── SourceCodePro-Regular.eot │ ├── SourceCodePro-Regular.ttf │ ├── SourceSansPro-Black.woff │ ├── SourceSansPro-Italic.eot │ ├── SourceSansPro-Italic.ttf │ ├── SourceSansPro-Italic.woff │ ├── SourceSansPro-Light.woff │ ├── SourceSansPro-Regular.eot │ ├── SourceSansPro-Regular.ttf │ ├── SourceSerifPro-Bold.woff │ ├── SourceCodePro-ExtraLight.eot │ ├── SourceCodePro-ExtraLight.ttf │ ├── SourceCodePro-Regular.woff │ ├── SourceCodePro-Semibold.eot │ ├── SourceCodePro-Semibold.ttf │ ├── SourceCodePro-Semibold.woff │ ├── SourceSansPro-BoldItalic.eot │ ├── SourceSansPro-BoldItalic.ttf │ ├── SourceSansPro-ExtraLight.eot │ ├── SourceSansPro-ExtraLight.ttf │ ├── SourceSansPro-Regular.woff │ ├── SourceSansPro-Semibold.eot │ ├── SourceSansPro-Semibold.ttf │ ├── SourceSansPro-Semibold.woff │ ├── SourceSerifPro-Regular.eot │ ├── SourceSerifPro-Regular.ttf │ ├── SourceSerifPro-Regular.woff │ ├── SourceSerifPro-Semibold.eot │ ├── SourceSerifPro-Semibold.ttf │ ├── SourceSerifPro-Semibold.woff │ ├── SourceCodePro-ExtraLight.woff │ ├── SourceSansPro-BlackItalic.eot │ ├── SourceSansPro-BlackItalic.ttf │ ├── SourceSansPro-BlackItalic.woff │ ├── SourceSansPro-BoldItalic.woff │ ├── SourceSansPro-ExtraLight.woff │ ├── SourceSansPro-LightItalic.eot │ ├── SourceSansPro-LightItalic.ttf │ ├── SourceSansPro-LightItalic.woff │ ├── SourceSansPro-SemiboldItalic.eot │ ├── SourceSansPro-SemiboldItalic.ttf │ ├── SourceSansPro-SemiboldItalic.woff │ ├── SourceSansPro-ExtraLightItalic.eot │ ├── SourceSansPro-ExtraLightItalic.ttf │ └── SourceSansPro-ExtraLightItalic.woff ├── markdown-lint │ ├── images │ │ ├── mark-error.png │ │ ├── mark-multiple.png │ │ ├── mark-warning.png │ │ ├── message-error.png │ │ └── message-warning.png │ └── css │ │ └── lint.css ├── vendor │ ├── jquery-ui │ │ └── images │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ ├── ui-icons_454545_256x240.png │ │ │ ├── ui-icons_888888_256x240.png │ │ │ ├── ui-icons_cd0a0a_256x240.png │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ │ └── ui-bg_highlight-soft_75_cccccc_1x100.png │ ├── ot │ │ ├── compress.sh │ │ ├── socketio-adapter.js │ │ └── wrapped-operation.js │ └── codemirror-spell-checker │ │ └── spell-checker.min.css ├── css │ ├── google-font.css │ ├── center.css │ ├── codemirror-extend │ │ ├── one-dark.css │ │ ├── tomorrow-night-bright.css │ │ ├── ayu-dark.css │ │ └── ayu-mirage.css │ ├── site.css │ └── slide-preview.css ├── .eslintrc.js ├── images │ └── mattermost-logo.svg └── docs │ └── privacy.md.example ├── .nvmrc ├── Aptfile ├── Procfile ├── config.js ├── .buildpacks ├── bin ├── heroku_start.sh ├── heroku └── setup ├── FUNDING.json ├── utils └── string.js ├── .github ├── tests │ ├── pull-request.json │ └── README.md └── workflows │ ├── check-release.yml │ └── build.yml ├── .dockerignore ├── lib ├── middleware │ ├── codiMDVersion.js │ ├── checkURIValid.js │ ├── tooBusy.js │ └── redirectWithoutTrailingSlashes.js ├── migrations │ ├── 20240114120250-revision-add-index.js │ ├── 20180525153000-user-add-delete-token.js │ ├── 20180306150303-fix-enum.js │ ├── 20150515125813-create-temp.js │ ├── 20180209120907-longtext-of-authorship.js │ ├── 20150508114741-create-notes.js │ ├── 20150504155329-create-users.js │ ├── 20161009040430-support-delete-note.js │ ├── 20180326103000-use-text-in-tokens.js │ ├── 20200104215332-remove-temp-table.js │ ├── 20150915153700-change-notes-title-to-text.js │ ├── 20160515114000-user-add-tokens.js │ ├── 20171009121200-longtext-for-mysql.js │ ├── 20160420180355-note-add-alias.js │ ├── 20160112220142-note-add-lastchange.js │ ├── 20161201050312-support-email-signin.js │ ├── 20160607060246-support-revision.js │ ├── 20160703062241-support-authorship.js │ └── 20150702001020-update-to-0_3_1.js ├── config │ ├── enum.js │ ├── defaultSSL.js │ └── utils.js ├── ot │ ├── index.js │ ├── server.js │ └── wrapped-operation.js ├── metrics.js ├── errorPage │ └── index.js ├── logger.js ├── imageRouter │ ├── imgur.js │ ├── lutim.js │ ├── azure.js │ ├── minio.js │ ├── filesystem.js │ ├── index.js │ └── s3.js ├── models │ ├── author.js │ └── index.js ├── auth │ ├── twitter │ │ └── index.js │ ├── facebook │ │ └── index.js │ ├── bitbucket │ │ └── index.js │ ├── dropbox │ │ └── index.js │ ├── google │ │ └── index.js │ ├── oauth2 │ │ └── index.js │ ├── gitlab │ │ └── index.js │ ├── mattermost │ │ └── index.js │ ├── openid │ │ └── index.js │ ├── index.js │ └── utils.js ├── homepage │ └── index.js ├── realtime │ ├── realtimeSaveRevisionJob.js │ ├── realtimeCleanDanglingUserJob.js │ └── realtimeUpdateDirtyNoteJob.js ├── utils.js ├── status │ └── index.js ├── letter-avatars.js └── web │ └── middleware │ └── checkVersion.js ├── .sequelizerc.example ├── test ├── testDoubles │ ├── realtimeJobStub.js │ ├── loggerFake.js │ ├── otFake.js │ └── ProcessQueueFake.js ├── realtime │ ├── utils.js │ ├── cleanDanglingUser.test.js │ ├── realtime.test.js │ └── saveRevisionJob.test.js ├── auth │ └── oauth2 │ │ └── strategy.test.js └── letter-avatars.js ├── .editorconfig ├── webpack.dev.js ├── deployments ├── docker-entrypoint.sh ├── build.sh ├── Dockerfile └── docker-compose.yml ├── babel.config.js ├── .gitignore ├── webpack.htmlexport.js ├── webpack.prod.js ├── .devcontainer ├── Dockerfile ├── docker-compose.yml └── devcontainer.json ├── .mailmap └── contribute └── developer-certificate-of-origin /public/default.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.2 2 | -------------------------------------------------------------------------------- /Aptfile: -------------------------------------------------------------------------------- 1 | libvips-dev 2 | -------------------------------------------------------------------------------- /public/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/views/codimd/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/views/index/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/views/index/header.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/heroku_start.sh 2 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const config = require('./lib/config') 2 | 3 | module.exports = config.db 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/screenshot.png -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/Scalingo/apt-buildpack 2 | https://github.com/Scalingo/nodejs-buildpack 3 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/codimd-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/codimd-icon-1024.png -------------------------------------------------------------------------------- /public/js/lib/editor/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | docmaxlength: null 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.woff -------------------------------------------------------------------------------- /bin/heroku_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | CMD_DB_URL="$DATABASE_URL" CMD_PORT="$PORT" npm run start 6 | -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.woff -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-error.png -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.woff -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-multiple.png -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-warning.png -------------------------------------------------------------------------------- /public/markdown-lint/images/message-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/message-error.png -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0xEd37B84FD84A834886aC07693aF6A9cd35040002" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.woff -------------------------------------------------------------------------------- /public/markdown-lint/images/message-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/message-warning.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /utils/string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function stripTags (s) { 4 | return s.replace(RegExp(']*>', 'gi'), '') 5 | } 6 | 7 | exports.stripTags = stripTags 8 | -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /public/js/lib/modeType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | edit: { 3 | name: 'edit' 4 | }, 5 | view: { 6 | name: 'view' 7 | }, 8 | both: { 9 | name: 'both' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/tests/pull-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "pull_request": { 3 | "head": { 4 | "ref": "release/1.2.3" 5 | }, 6 | "base": { 7 | "ref": "master" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/views/includes/header.ejs: -------------------------------------------------------------------------------- 1 | <% for (var css in htmlWebpackPlugin.files.css) { %> 2 | 3 | <% } %> 4 | -------------------------------------------------------------------------------- /public/css/google-font.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,300italic,300|Source+Serif+Pro|Source+Code+Pro:400,300,500&subset=latin,latin-ext); 2 | -------------------------------------------------------------------------------- /public/js/lib/appState.js: -------------------------------------------------------------------------------- 1 | import modeType from './modeType' 2 | 3 | const state = { 4 | syncscroll: true, 5 | currentMode: modeType.view, 6 | nightMode: false 7 | } 8 | 9 | export default state 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage 3 | node_modules/ 4 | 5 | # ignore config files 6 | config.json 7 | .sequelizerc 8 | 9 | # ignore webpack build 10 | public/build 11 | public/views/build 12 | 13 | .nyc_output 14 | coverage/ 15 | -------------------------------------------------------------------------------- /public/js/htmlExport.js: -------------------------------------------------------------------------------- 1 | require('../css/github-extract.css') 2 | require('../css/markdown.css') 3 | require('../css/extra.css') 4 | require('../css/slide-preview.css') 5 | require('../css/google-font.css') 6 | require('../css/site.css') 7 | -------------------------------------------------------------------------------- /.github/tests/README.md: -------------------------------------------------------------------------------- 1 | # Test github actions with act 2 | 3 | ```bash 4 | act pull_request --container-architecture linux/arm64 -e .github/tests/pull-request.json -j ch 5 | eck-release-pr -P ubuntu-latest=catthehacker/ubuntu:act-latest 6 | ``` 7 | -------------------------------------------------------------------------------- /lib/middleware/codiMDVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | 5 | module.exports = function (req, res, next) { 6 | res.set({ 7 | 'CodiMD-Version': config.version 8 | }) 9 | return next() 10 | } 11 | -------------------------------------------------------------------------------- /public/views/includes/scripts.ejs: -------------------------------------------------------------------------------- 1 | 2 | <% for (var js in htmlWebpackPlugin.files.js) { %> 3 | 4 | <% } %> 5 | -------------------------------------------------------------------------------- /bin/heroku: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ ! -z "$DYNO" ]; then 6 | # setup config files 7 | cp .sequelizerc.example .sequelizerc 8 | 9 | cat << EOF > config.json 10 | 11 | { 12 | "production": { 13 | } 14 | } 15 | 16 | EOF 17 | 18 | fi 19 | -------------------------------------------------------------------------------- /public/css/center.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .container-fluid { 4 | height: 98%; 5 | } 6 | .container-fluid { 7 | display: table; 8 | vertical-align: middle; 9 | } 10 | .vertical-center-row { 11 | display: table-cell; 12 | vertical-align: middle; 13 | } -------------------------------------------------------------------------------- /public/js/mathjax-config-extra.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | messageStyle: 'none', 3 | skipStartupTypeset: true, 4 | tex2jax: { 5 | inlineMath: [['$', '$'], ['\\(', '\\)']], 6 | processEscapes: true 7 | }, 8 | TeX: { 9 | extensions: ['mhchem.js'] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dotWideMiddle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/vendor/ot/compress.sh: -------------------------------------------------------------------------------- 1 | uglifyjs --compress --mangle --output ot.min.js \ 2 | ./text-operation.js \ 3 | ./selection.js \ 4 | ./wrapped-operation.js \ 5 | ./undo-manager.js \ 6 | ./client.js \ 7 | ./codemirror-adapter.js \ 8 | ./socketio-adapter.js \ 9 | ./ajax-adapter.js \ 10 | ./editor-client.js -------------------------------------------------------------------------------- /.sequelizerc.example: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('./lib/config') 3 | 4 | module.exports = { 5 | config: path.resolve('config.js'), 6 | 'migrations-path': path.resolve('lib', 'migrations'), 7 | 'models-path': path.resolve('lib', 'models'), 8 | url: config.dbURL 9 | } 10 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/one-dark.css: -------------------------------------------------------------------------------- 1 | .cm-s-one-dark .CodeMirror-linenumber { 2 | color: #676767; 3 | } 4 | 5 | .cm-s-one-dark.CodeMirror-focused 6 | .CodeMirror-activeline 7 | .CodeMirror-gutter-elt { 8 | color: #b0b0b0; 9 | } 10 | 11 | .cm-s-one-dark .cm-comment { 12 | color: #818895; 13 | } 14 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dotWideLeft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/migrations/20240114120250-revision-add-index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addIndex('Revisions', ['noteId'], {}) 6 | }, 7 | 8 | down: (queryInterface, Sequelize) => { 9 | return queryInterface.removeIndex('Revisions', 'noteId') 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dotWideRight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('index/head') %> 6 | 7 | 8 | 9 | <%- include('index/header') %> 10 | <%- include('index/body') %> 11 | <%- include('index/footer') %> 12 | <%- include('index/foot') %> 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/testDoubles/realtimeJobStub.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class realtimeJobStub { 4 | start () { 5 | } 6 | 7 | stop () { 8 | } 9 | } 10 | 11 | exports.realtimeJobStub = realtimeJobStub 12 | exports.UpdateDirtyNoteJob = realtimeJobStub 13 | exports.CleanDanglingUserJob = realtimeJobStub 14 | exports.SaveRevisionJob = realtimeJobStub 15 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dotEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dotEmpty_h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/dot_h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/views/codimd.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('codimd/head') %> 6 | 7 | 8 | 9 | <%- include('codimd/header') %> 10 | <%- include('codimd/body') %> 11 | <%- include('codimd/footer') %> 12 | <%- include('codimd/foot') %> 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/testDoubles/loggerFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | 5 | function createFakeLogger () { 6 | return { 7 | error: sinon.stub(), 8 | warn: sinon.stub(), 9 | info: sinon.stub(), 10 | debug: sinon.stub(), 11 | log: sinon.stub() 12 | } 13 | } 14 | 15 | exports.createFakeLogger = createFakeLogger 16 | -------------------------------------------------------------------------------- /lib/config/enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.Environment = { 4 | development: 'development', 5 | production: 'production', 6 | test: 'test' 7 | } 8 | 9 | exports.Permission = { 10 | freely: 'freely', 11 | editable: 'editable', 12 | limited: 'limited', 13 | locked: 'locked', 14 | protected: 'protected', 15 | private: 'private' 16 | } 17 | -------------------------------------------------------------------------------- /public/vendor/codemirror-spell-checker/spell-checker.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * codemirror-spell-checker v1.0.6 3 | * Copyright Next Step Webs, Inc. 4 | * @link https://github.com/NextStepWebs/codemirror-spell-checker 5 | * @license MIT 6 | */ 7 | .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){border-bottom:2px dotted rgba(255,0,0,.8)} -------------------------------------------------------------------------------- /lib/middleware/checkURIValid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../logger') 4 | const response = require('../response') 5 | 6 | module.exports = function (req, res, next) { 7 | try { 8 | decodeURIComponent(req.path) 9 | } catch (err) { 10 | logger.error(err) 11 | return response.errorBadRequest(req, res) 12 | } 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /lib/middleware/tooBusy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const toobusy = require('toobusy-js') 4 | 5 | const config = require('../config') 6 | const response = require('../response') 7 | 8 | toobusy.maxLag(config.responseMaxLag) 9 | 10 | module.exports = function (req, res, next) { 11 | if (toobusy()) { 12 | response.errorServiceUnavailable(req, res) 13 | } else { 14 | next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/ot/index.js: -------------------------------------------------------------------------------- 1 | exports.version = '0.0.15'; 2 | 3 | exports.TextOperation = require('./text-operation'); 4 | exports.SimpleTextOperation = require('./simple-text-operation'); 5 | exports.Client = require('./client'); 6 | exports.Server = require('./server'); 7 | exports.Selection = require('./selection'); 8 | exports.EditorSocketIOServer = require('./editor-socketio-server'); 9 | -------------------------------------------------------------------------------- /public/views/shared/ga.ejs: -------------------------------------------------------------------------------- 1 | <% if(typeof GA !== 'undefined' && GA) { %> 2 | 4 | 11 | <% } %> 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{*.html,*.ejs}] 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/string_o.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Router } = require('express') 4 | 5 | const { wrap } = require('./utils') 6 | 7 | // load controller 8 | const statusController = require('./status') 9 | const appRouter = Router() 10 | 11 | // register route 12 | appRouter.get('/status', wrap(statusController.getStatus)) 13 | appRouter.get('/metrics/codimd', wrap(statusController.getMetrics)) 14 | 15 | exports.router = appRouter 16 | -------------------------------------------------------------------------------- /lib/migrations/20180525153000-user-add-delete-token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'deleteToken', { 5 | type: Sequelize.UUID, 6 | defaultValue: Sequelize.UUIDV4 7 | }) 8 | }, 9 | 10 | down: function (queryInterface, Sequelize) { 11 | return queryInterface.removeColumn('Users', 'deleteToken') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js') 2 | const htmlexport = require('./webpack.htmlexport') 3 | const merge = require('webpack-merge') 4 | 5 | module.exports = [ 6 | // merge common config 7 | merge(common, { 8 | mode: 'development', 9 | devtool: 'cheap-module-eval-source-map' 10 | }), 11 | merge(htmlexport, { 12 | mode: 'development', 13 | devtool: 'cheap-module-eval-source-map' 14 | })] 15 | -------------------------------------------------------------------------------- /deployments/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [[ "$#" -gt 0 ]]; then 6 | exec "$@" 7 | exit $? 8 | fi 9 | 10 | # check database and redis is ready 11 | pcheck -env CMD_DB_URL 12 | 13 | # run DB migrate 14 | NEED_MIGRATE=${CMD_AUTO_MIGRATE:=true} 15 | 16 | if [[ "$NEED_MIGRATE" = "true" ]] && [[ -f .sequelizerc ]] ; then 17 | npx sequelize db:migrate 18 | fi 19 | 20 | # start application 21 | node app.js 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | targets: { 5 | node: '14' 6 | }, 7 | useBuiltIns: 'usage', 8 | corejs: 3, 9 | modules: 'auto' 10 | }] 11 | ], 12 | plugins: [ 13 | ['@babel/plugin-transform-runtime', { 14 | corejs: 3 15 | }], 16 | '@babel/plugin-transform-nullish-coalescing-operator', 17 | '@babel/plugin-transform-optional-chaining' 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /lib/config/defaultSSL.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | function getFile (path) { 6 | if (fs.existsSync(path)) { 7 | return path 8 | } 9 | return undefined 10 | } 11 | 12 | module.exports = { 13 | sslKeyPath: getFile('/run/secrets/key.pem'), 14 | sslCertPath: getFile('/run/secrets/cert.pem'), 15 | sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], 16 | dhParamPath: getFile('/run/secrets/dhparam.pem') 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | composer.phar 3 | composer.lock 4 | .env.*.php 5 | .env.php 6 | .DS_Store 7 | .idea/ 8 | Thumbs.db 9 | npm-debug.log 10 | hackmd_io 11 | newrelic_agent.log 12 | logs/ 13 | tmp/ 14 | backups/ 15 | *.pid 16 | *.log 17 | *.sqlite 18 | 19 | # ignore config files 20 | config.json 21 | .sequelizerc 22 | 23 | # ignore webpack build 24 | public/build 25 | public/views/build 26 | 27 | public/uploads/* 28 | !public/uploads/.gitkeep 29 | /.nyc_output 30 | /coverage/ 31 | 32 | .vscode/settings.json -------------------------------------------------------------------------------- /lib/migrations/20180306150303-fix-enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async function (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') }) 6 | }, 7 | 8 | down: async function (queryInterface, Sequelize) { 9 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/migrations/20150515125813-create-temp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Temp', { 5 | id: { 6 | type: Sequelize.STRING, 7 | primaryKey: true 8 | }, 9 | date: Sequelize.TEXT, 10 | createdAt: Sequelize.DATE, 11 | updatedAt: Sequelize.DATE 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.dropTable('Temp') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/string_x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('codimd/head') %> 6 | 7 | 8 | 9 | 10 | <%- include('codimd/header') %> 11 |
12 |
13 |

<%- code %> <%- detail %> <%- msg %>

14 |
15 |
16 | <%- include('codimd/footer') %> 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/testDoubles/otFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | 5 | class EditorSocketIOServerFake { 6 | constructor () { 7 | this.addClient = sinon.stub() 8 | this.onOperation = sinon.stub() 9 | this.onGetOperations = sinon.stub() 10 | this.updateSelection = sinon.stub() 11 | this.setName = sinon.stub() 12 | this.setColor = sinon.stub() 13 | this.getClient = sinon.stub() 14 | this.onDisconnect = sinon.stub() 15 | } 16 | } 17 | 18 | exports.EditorSocketIOServer = EditorSocketIOServerFake 19 | -------------------------------------------------------------------------------- /public/js/lib/common/constant.ejs: -------------------------------------------------------------------------------- 1 | window.domain = '<%- domain %>' 2 | window.urlpath = '<%- urlpath %>' 3 | window.debug = <%- debug %> 4 | window.version = '<%- version %>' 5 | window.plantumlServer = '<%- plantumlServer %>' 6 | 7 | window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %> 8 | 9 | window.defaultUseHardbreak = <%- defaultUseHardbreak %> 10 | 11 | window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>' 12 | 13 | window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' 14 | 15 | window.USE_CDN = <%- useCDN %> 16 | 17 | window.defaultTocDepth = <%- defaultTocDepth %> 18 | -------------------------------------------------------------------------------- /lib/middleware/redirectWithoutTrailingSlashes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | 5 | module.exports = function (req, res, next) { 6 | if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { 7 | const queryString = req.url.slice(req.path.length) 8 | const urlPath = req.path.slice(0, -1) 9 | let serverURL = config.serverURL 10 | if (config.urlPath) { 11 | serverURL = serverURL.slice(0, -(config.urlPath.length + 1)) 12 | } 13 | res.redirect(301, serverURL + urlPath + queryString) 14 | } else { 15 | next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/migrations/20180209120907-longtext-of-authorship.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async function (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') }) 6 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') }) 7 | }, 8 | 9 | down: async function (queryInterface, Sequelize) { 10 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT }) 11 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deployments/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | set -x 5 | 6 | if [[ -z $1 || -z $2 ]];then 7 | echo "build.sh [runtime image] [buildpack image]" 8 | exit 1 9 | fi 10 | 11 | CURRENT_DIR=$(dirname "$BASH_SOURCE") 12 | 13 | GIT_SHA1="$(git rev-parse HEAD)" 14 | GIT_SHORT_ID="${GIT_SHA1:0:8}" 15 | GIT_TAG=$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || echo "") 16 | 17 | DOCKER_TAG="${GIT_TAG:-$GIT_SHORT_ID}" 18 | 19 | docker build --build-arg RUNTIME=$1 --build-arg BUILDPACK=$2 -t "hackmdio/hackmd:$DOCKER_TAG" -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.." 20 | 21 | -------------------------------------------------------------------------------- /lib/migrations/20150508114741-create-notes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Notes', { 5 | id: { 6 | type: Sequelize.UUID, 7 | primaryKey: true, 8 | defaultValue: Sequelize.UUIDV4 9 | }, 10 | ownerId: Sequelize.UUID, 11 | content: Sequelize.TEXT, 12 | title: Sequelize.STRING, 13 | createdAt: Sequelize.DATE, 14 | updatedAt: Sequelize.DATE 15 | }) 16 | }, 17 | 18 | down: function (queryInterface, Sequelize) { 19 | return queryInterface.dropTable('Notes') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/testDoubles/ProcessQueueFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ProcessQueueFake { 4 | constructor () { 5 | this.taskMap = new Map() 6 | this.queue = [] 7 | } 8 | 9 | start () { 10 | 11 | } 12 | 13 | stop () { 14 | 15 | } 16 | 17 | checkTaskIsInQueue (id) { 18 | return this.taskMap.has(id) 19 | } 20 | 21 | push (id, processFunc) { 22 | this.queue.push({ 23 | id: id, 24 | processFunc: processFunc 25 | }) 26 | this.taskMap.set(id, true) 27 | } 28 | 29 | process () { 30 | 31 | } 32 | } 33 | 34 | exports.ProcessQueueFake = ProcessQueueFake 35 | exports.ProcessQueue = ProcessQueueFake 36 | -------------------------------------------------------------------------------- /lib/errorPage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | const { responseError } = require('../response') 5 | 6 | exports.errorForbidden = (req, res) => { 7 | if (req.user) { 8 | return responseError(res, '403', 'Forbidden', 'oh no.') 9 | } 10 | 11 | req.flash('error', 'You are not allowed to access this page. Maybe try logging in?') 12 | res.redirect(config.serverURL + '/') 13 | } 14 | 15 | exports.errorNotFound = (req, res) => { 16 | responseError(res, '404', 'Not Found', 'oops.') 17 | } 18 | 19 | exports.errorInternalError = (req, res) => { 20 | responseError(res, '500', 'Internal Error', 'wtf.') 21 | } 22 | -------------------------------------------------------------------------------- /webpack.htmlexport.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | name: 'save-as-html', 6 | entry: { 7 | htmlExport: path.join(__dirname, 'public/js/htmlExport.js') 8 | }, 9 | module: { 10 | rules: [{ 11 | test: /\.css$/, 12 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 13 | }] 14 | }, 15 | output: { 16 | path: path.join(__dirname, 'public/build'), 17 | publicPath: '/build/', 18 | filename: '[name].js' 19 | }, 20 | plugins: [ 21 | new MiniCssExtractPlugin({ 22 | filename: 'html.min.css' 23 | }) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createLogger, format, transports } = require('winston') 3 | 4 | const logger = createLogger({ 5 | level: 'debug', 6 | format: format.combine( 7 | format.uncolorize(), 8 | format.timestamp(), 9 | format.align(), 10 | format.splat(), 11 | format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`) 12 | ), 13 | transports: [ 14 | new transports.Console({ 15 | handleExceptions: true 16 | }) 17 | ], 18 | exitOnError: false 19 | }) 20 | 21 | logger.stream = { 22 | write: function (message, encoding) { 23 | logger.info(message) 24 | } 25 | } 26 | 27 | module.exports = logger 28 | -------------------------------------------------------------------------------- /lib/migrations/20150504155329-create-users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Users', { 5 | id: { 6 | type: Sequelize.UUID, 7 | primaryKey: true, 8 | defaultValue: Sequelize.UUIDV4 9 | }, 10 | profileid: { 11 | type: Sequelize.STRING, 12 | unique: true 13 | }, 14 | profile: Sequelize.TEXT, 15 | history: Sequelize.TEXT, 16 | createdAt: Sequelize.DATE, 17 | updatedAt: Sequelize.DATE 18 | }) 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | return queryInterface.dropTable('Users') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js') 2 | const htmlexport = require('./webpack.htmlexport') 3 | const merge = require('webpack-merge') 4 | const path = require('path') 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 6 | 7 | module.exports = [ 8 | merge(common, { 9 | mode: 'production', 10 | output: { 11 | path: path.join(__dirname, 'public/build'), 12 | publicPath: '/build/', 13 | filename: '[name].[contenthash].js' 14 | } 15 | }), 16 | merge(htmlexport, { 17 | mode: 'production', 18 | optimization: { 19 | minimizer: [ 20 | new OptimizeCSSAssetsPlugin({}) 21 | ] 22 | } 23 | })] 24 | -------------------------------------------------------------------------------- /public/views/shared/disqus.ejs: -------------------------------------------------------------------------------- 1 |
2 | 13 | 19 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version: 16, 14 2 | ARG VARIANT=14-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | # [Optional] Uncomment if you want to install an additional version of node using nvm 10 | # ARG EXTRA_NODE_VERSION=10 11 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 12 | 13 | # [Optional] Uncomment if you want to install more global node modules 14 | RUN su node -c "npm install -g npm@6" 15 | -------------------------------------------------------------------------------- /lib/migrations/20161009040430-support-delete-note.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) { 5 | 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') { 6 | console.log('Migration has already run… ignoring.') 7 | } else { 8 | throw error 9 | } 10 | }) 11 | }, 12 | 13 | down: function (queryInterface, Sequelize) { 14 | return queryInterface.removeColumn('Notes', 'deletedAt') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/migrations/20180326103000-use-text-in-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.changeColumn('Users', 'accessToken', { 6 | type: Sequelize.TEXT 7 | }).then(function () { 8 | return queryInterface.changeColumn('Users', 'refreshToken', { 9 | type: Sequelize.TEXT 10 | }) 11 | }) 12 | }, 13 | 14 | down: function (queryInterface, Sequelize) { 15 | return queryInterface.changeColumn('Users', 'accessToken', { 16 | type: Sequelize.STRING 17 | }).then(function () { 18 | return queryInterface.changeColumn('Users', 'refreshToken', { 19 | type: Sequelize.STRING 20 | }) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/migrations/20200104215332-remove-temp-table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.dropTable('Temp') 6 | /* 7 | Add altering commands here. 8 | Return a promise to correctly handle asynchronicity. 9 | 10 | Example: 11 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 12 | */ 13 | }, 14 | 15 | down: (queryInterface, Sequelize) => { 16 | return queryInterface.createTable('Temp', { 17 | id: { 18 | type: Sequelize.STRING, 19 | primaryKey: true 20 | }, 21 | date: Sequelize.TEXT, 22 | createdAt: Sequelize.DATE, 23 | updatedAt: Sequelize.DATE 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/views/shared/polyfill.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /public/js/lib/config/index.js: -------------------------------------------------------------------------------- 1 | export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || '' 2 | 3 | export const domain = window.domain || '' // domain name 4 | export const urlpath = window.urlpath || '' // sub url path, like: www.example.com/ 5 | export const debug = window.debug || false 6 | 7 | export const port = window.location.port 8 | export const serverurl = `${window.location.protocol}//${domain || window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}` 9 | window.serverurl = serverurl 10 | export const noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]) 11 | export const noteurl = `${serverurl}/${noteid}` 12 | 13 | export const version = window.version 14 | -------------------------------------------------------------------------------- /public/css/site.css: -------------------------------------------------------------------------------- 1 | /* for all pages should include this */ 2 | body { 3 | font-smoothing: subpixel-antialiased !important; 4 | -webkit-font-smoothing: subpixel-antialiased !important; 5 | -moz-osx-font-smoothing: auto !important; 6 | text-shadow: 0 0 1em transparent, 1px 1px 1.2px rgba(0, 0, 0, 0.004); 7 | /*text-rendering: optimizeLegibility;*/ 8 | -webkit-overflow-scrolling: touch; 9 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 10 | letter-spacing: 0.025em; 11 | } 12 | :focus, .focus { 13 | outline: none !important; 14 | } 15 | ::-moz-focus-inner { 16 | border: 0 !important; 17 | } 18 | 19 | /* manual fix for bootstrap issue 14040, there is an unnecessary padding-right on modal open */ 20 | body.modal-open { 21 | overflow-y: auto; 22 | padding-right: 0 !important; 23 | } 24 | -------------------------------------------------------------------------------- /public/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // this config file is used in concert with the root .eslintrc.js in the root dir. 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | }, 11 | "globals": { 12 | "$": false, 13 | "CodeMirror": false, 14 | "Cookies": false, 15 | "moment": false, 16 | "editor": false, 17 | "ui": false, 18 | "modeType": false, 19 | "serverurl": false, 20 | "key": false, 21 | "gapi": false, 22 | "Dropbox": false, 23 | "FilePicker": false, 24 | "ot": false, 25 | "MediaUploader": false, 26 | "hex2rgb": false, 27 | "num_loaded": false, 28 | "Visibility": false, 29 | "inlineAttachment": false 30 | }, 31 | "rules": { 32 | "no-unused-vars": "warn" 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Max Wu Wu Cheng-Han 2 | Max Wu Cheng-Han, Wu 3 | Max Wu jackycute 4 | Max Wu Wu, Cheng-Han 5 | Max Wu jackycute 6 | 7 | Sheogorath Christoph (Sheogorath) Kern 8 | 9 | Raccoon Raccoon Li 10 | Raccoon Raccoon 11 | 12 | Peter Dave Hello Peter Dave Hello 13 | 14 | Claudius Coenen Claudius Coenen 15 | -------------------------------------------------------------------------------- /lib/migrations/20150915153700-change-notes-title-to-text.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const isSQLite = require('../utils').isSQLite 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.changeColumn('Notes', 'title', { 6 | type: Sequelize.TEXT 7 | }).then(function () { 8 | if (isSQLite(queryInterface.sequelize)) { 9 | // manual added index will be removed in sqlite 10 | return queryInterface.addIndex('Notes', ['shortid']) 11 | } 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.changeColumn('Notes', 'title', { 17 | type: Sequelize.STRING 18 | }).then(function () { 19 | if (isSQLite(queryInterface.sequelize)) { 20 | // manual added index will be removed in sqlite 21 | return queryInterface.addIndex('Notes', ['shortid']) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/imageRouter/imgur.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const config = require('../config') 3 | const logger = require('../logger') 4 | 5 | const imgur = require('@hackmd/imgur') 6 | 7 | exports.uploadImage = function (imagePath, callback) { 8 | if (!imagePath || typeof imagePath !== 'string') { 9 | callback(new Error('Image path is missing or wrong'), null) 10 | return 11 | } 12 | 13 | if (!callback || typeof callback !== 'function') { 14 | logger.error('Callback has to be a function') 15 | return 16 | } 17 | 18 | imgur.setClientId(config.imgur.clientID) 19 | imgur.uploadFile(imagePath) 20 | .then(function (json) { 21 | if (config.debug) { 22 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) 23 | } 24 | callback(null, json.data.link.replace(/^http:\/\//i, 'https://')) 25 | }).catch(function (err) { 26 | callback(new Error(err), null) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/imageRouter/lutim.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const config = require('../config') 3 | const logger = require('../logger') 4 | 5 | const lutim = require('lutim') 6 | 7 | exports.uploadImage = function (imagePath, callback) { 8 | if (!imagePath || typeof imagePath !== 'string') { 9 | callback(new Error('Image path is missing or wrong'), null) 10 | return 11 | } 12 | 13 | if (!callback || typeof callback !== 'function') { 14 | logger.error('Callback has to be a function') 15 | return 16 | } 17 | 18 | if (config.lutim && config.lutim.url) { 19 | lutim.setAPIUrl(config.lutim.url) 20 | } 21 | 22 | lutim.uploadImage(imagePath) 23 | .then(function (json) { 24 | if (config.debug) { 25 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) 26 | } 27 | callback(null, lutim.getAPIUrl() + json.msg.short) 28 | }).catch(function (err) { 29 | callback(new Error(err), null) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/migrations/20160515114000-user-add-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () { 5 | return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) 6 | }).catch(function (error) { 7 | 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') { 8 | console.log('Migration has already run… ignoring.') 9 | } else { 10 | throw error 11 | } 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.removeColumn('Users', 'accessToken').then(function () { 17 | return queryInterface.removeColumn('Users', 'refreshToken') 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/migrations/20171009121200-longtext-for-mysql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async function (queryInterface, Sequelize) { 4 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') }) 5 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') }) 6 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') }) 7 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') }) 8 | }, 9 | 10 | down: async function (queryInterface, Sequelize) { 11 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT }) 12 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT }) 13 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT }) 14 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/migrations/20160420180355-note-add-alias.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'alias', { 5 | type: Sequelize.STRING 6 | }).then(function () { 7 | return queryInterface.addIndex('Notes', ['alias'], { 8 | indicesType: 'UNIQUE' 9 | }) 10 | }).catch(function (error) { 11 | 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') { 12 | console.log('Migration has already run… ignoring.') 13 | } else { 14 | throw error 15 | } 16 | }) 17 | }, 18 | 19 | down: function (queryInterface, Sequelize) { 20 | return queryInterface.removeColumn('Notes', 'alias').then(function () { 21 | return queryInterface.removeIndex('Notes', ['alias']) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/js/lib/editor/constants.js: -------------------------------------------------------------------------------- 1 | import { serverurl } from '../config' 2 | 3 | export const availableThemes = [ 4 | { name: 'Light', value: 'default' }, 5 | { name: 'One Dark (Default)', value: 'one-dark' }, 6 | { name: 'Monokai', value: 'monokai' }, 7 | { name: 'Solarized Dark', value: 'solarized dark' }, 8 | { name: 'Solarized Light', value: 'solarized light' }, 9 | { name: 'Dracula', value: 'dracula' }, 10 | { name: 'Material', value: 'material' }, 11 | { name: 'Nord', value: 'nord' }, 12 | { name: 'Panda', value: 'panda-syntax' }, 13 | { name: 'Ayu Dark', value: 'ayu-dark' }, 14 | { name: 'Ayu Mirage', value: 'ayu-mirage' }, 15 | { name: 'Tomorror Night Bright', value: 'tomorrow-night-bright' }, 16 | { name: 'Tomorror Night Eighties', value: 'tomorrow-night-eighties' } 17 | ] 18 | 19 | export const emojifyImageDir = window.USE_CDN ? 'https://cdn.jsdelivr.net/npm/@hackmd/emojify.js@2.1.0/dist/images/basic' : `${serverurl}/build/emojify.js/dist/images/basic` 20 | -------------------------------------------------------------------------------- /public/js/locale.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, jquery */ 2 | /* global Cookies */ 3 | 4 | var lang = 'en' 5 | var userLang = navigator.language || navigator.userLanguage 6 | var userLangCode = userLang.split('-')[0] 7 | var locale = $('.ui-locale') 8 | var supportLangs = [] 9 | $('.ui-locale option').each(function () { 10 | supportLangs.push($(this).val()) 11 | }) 12 | if (Cookies.get('locale')) { 13 | lang = Cookies.get('locale') 14 | if (lang === 'zh') { 15 | lang = 'zh-TW' 16 | } 17 | } else if (supportLangs.indexOf(userLang) !== -1) { 18 | lang = supportLangs[supportLangs.indexOf(userLang)] 19 | } else if (supportLangs.indexOf(userLangCode) !== -1) { 20 | lang = supportLangs[supportLangs.indexOf(userLangCode)] 21 | } 22 | 23 | locale.val(lang) 24 | $('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected') 25 | 26 | locale.change(function () { 27 | Cookies.set('locale', $(this).val(), { 28 | expires: 365 29 | }) 30 | window.location.reload() 31 | }) 32 | -------------------------------------------------------------------------------- /lib/models/author.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | var Sequelize = require('sequelize') 4 | 5 | module.exports = function (sequelize, DataTypes) { 6 | var Author = sequelize.define('Author', { 7 | id: { 8 | type: Sequelize.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | color: { 13 | type: DataTypes.STRING 14 | } 15 | }, { 16 | indexes: [ 17 | { 18 | unique: true, 19 | fields: ['noteId', 'userId'] 20 | } 21 | ] 22 | }) 23 | 24 | Author.associate = function (models) { 25 | Author.belongsTo(models.Note, { 26 | foreignKey: 'noteId', 27 | as: 'note', 28 | constraints: false, 29 | onDelete: 'CASCADE', 30 | hooks: true 31 | }) 32 | Author.belongsTo(models.User, { 33 | foreignKey: 'userId', 34 | as: 'user', 35 | constraints: false, 36 | onDelete: 'CASCADE', 37 | hooks: true 38 | }) 39 | } 40 | 41 | return Author 42 | } 43 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # run command at repo root 6 | CURRENT_PATH=$PWD 7 | if [ -d .git ]; then 8 | cd "$(git rev-parse --show-toplevel)" 9 | fi 10 | 11 | if ! type npm > /dev/null 12 | then 13 | cat << EOF 14 | npm is not installed, please install Node.js and npm. 15 | Read more on Node.js official website: https://nodejs.org 16 | Setup will not be run 17 | EOF 18 | exit 0 19 | fi 20 | 21 | echo "copy config files" 22 | if [ ! -f config.json ]; then 23 | cp config.json.example config.json 24 | fi 25 | 26 | if [ ! -f .sequelizerc ]; then 27 | cp .sequelizerc.example .sequelizerc 28 | fi 29 | 30 | echo "install packages" 31 | npm install 32 | 33 | cat << EOF 34 | 35 | 36 | Edit the following config file to setup CodiMD server and client. 37 | Read more info at https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration 38 | 39 | * config.json -- CodiMD config 40 | * .sequelizerc -- db config 41 | 42 | EOF 43 | 44 | # change directory back 45 | cd "$CURRENT_PATH" 46 | -------------------------------------------------------------------------------- /lib/auth/twitter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const TwitterStrategy = require('passport-twitter').Strategy 6 | 7 | const config = require('../../config') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | 10 | const twitterAuth = module.exports = Router() 11 | 12 | passport.use(new TwitterStrategy({ 13 | consumerKey: config.twitter.consumerKey, 14 | consumerSecret: config.twitter.consumerSecret, 15 | callbackURL: config.serverURL + '/auth/twitter/callback', 16 | state: true 17 | }, passportGeneralCallback)) 18 | 19 | twitterAuth.get('/auth/twitter', function (req, res, next) { 20 | setReturnToFromReferer(req) 21 | passport.authenticate('twitter')(req, res, next) 22 | }) 23 | 24 | // twitter auth callback 25 | twitterAuth.get('/auth/twitter/callback', 26 | passport.authenticate('twitter', { 27 | successReturnToOrRedirect: config.serverURL + '/', 28 | failureRedirect: config.serverURL + '/' 29 | }) 30 | ) 31 | -------------------------------------------------------------------------------- /lib/auth/facebook/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const FacebookStrategy = require('passport-facebook').Strategy 6 | 7 | const config = require('../../config') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | 10 | const facebookAuth = module.exports = Router() 11 | 12 | passport.use(new FacebookStrategy({ 13 | clientID: config.facebook.clientID, 14 | clientSecret: config.facebook.clientSecret, 15 | callbackURL: config.serverURL + '/auth/facebook/callback', 16 | state: true 17 | }, passportGeneralCallback)) 18 | 19 | facebookAuth.get('/auth/facebook', function (req, res, next) { 20 | setReturnToFromReferer(req) 21 | passport.authenticate('facebook')(req, res, next) 22 | }) 23 | 24 | // facebook auth callback 25 | facebookAuth.get('/auth/facebook/callback', 26 | passport.authenticate('facebook', { 27 | successReturnToOrRedirect: config.serverURL + '/', 28 | failureRedirect: config.serverURL + '/' 29 | }) 30 | ) 31 | -------------------------------------------------------------------------------- /deployments/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUNTIME 2 | ARG BUILDPACK 3 | 4 | FROM $BUILDPACK as BUILD 5 | 6 | COPY --chown=hackmd:hackmd . . 7 | ENV QT_QPA_PLATFORM=offscreen 8 | 9 | RUN set -xe && \ 10 | git reset --hard && \ 11 | git clean -fx && \ 12 | npm install && \ 13 | npm run build && \ 14 | cp ./deployments/docker-entrypoint.sh ./ && \ 15 | cp .sequelizerc.example .sequelizerc && \ 16 | rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \ 17 | test docs contribute \ 18 | package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \ 19 | config.json.example README.md CONTRIBUTING.md AUTHORS node_modules 20 | 21 | FROM $RUNTIME 22 | USER hackmd 23 | ENV QT_QPA_PLATFORM=offscreen 24 | WORKDIR /home/hackmd/app 25 | COPY --chown=1500:1500 --from=BUILD /home/hackmd/app . 26 | RUN npm install --production && npm cache clean --force && rm -rf /tmp/{core-js-banners,phantomjs} 27 | EXPOSE 3000 28 | ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"] 29 | -------------------------------------------------------------------------------- /lib/migrations/20160112220142-note-add-lastchange.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'lastchangeuserId', { 5 | type: Sequelize.UUID 6 | }).then(function () { 7 | return queryInterface.addColumn('Notes', 'lastchangeAt', { 8 | type: Sequelize.DATE 9 | }) 10 | }).catch(function (error) { 11 | 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') { 12 | console.log('Migration has already run… ignoring.') 13 | } else { 14 | throw error 15 | } 16 | }) 17 | }, 18 | 19 | down: function (queryInterface, Sequelize) { 20 | return queryInterface.removeColumn('Notes', 'lastchangeAt') 21 | .then(function () { 22 | return queryInterface.removeColumn('Notes', 'lastchangeuserId') 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/auth/bitbucket/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const bitbucketAuth = module.exports = Router() 10 | 11 | passport.use(new BitbucketStrategy({ 12 | clientID: config.bitbucket.clientID, 13 | clientSecret: config.bitbucket.clientSecret, 14 | callbackURL: config.serverURL + '/auth/bitbucket/callback', 15 | state: true 16 | }, passportGeneralCallback)) 17 | 18 | bitbucketAuth.get('/auth/bitbucket', function (req, res, next) { 19 | setReturnToFromReferer(req) 20 | passport.authenticate('bitbucket')(req, res, next) 21 | }) 22 | 23 | // bitbucket auth callback 24 | bitbucketAuth.get('/auth/bitbucket/callback', 25 | passport.authenticate('bitbucket', { 26 | successReturnToOrRedirect: config.serverURL + '/', 27 | failureRedirect: config.serverURL + '/' 28 | }) 29 | ) 30 | -------------------------------------------------------------------------------- /lib/auth/dropbox/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const DropboxStrategy = require('passport-dropbox-oauth2').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const dropboxAuth = module.exports = Router() 10 | 11 | passport.use(new DropboxStrategy({ 12 | apiVersion: '2', 13 | clientID: config.dropbox.clientID, 14 | clientSecret: config.dropbox.clientSecret, 15 | callbackURL: config.serverURL + '/auth/dropbox/callback', 16 | state: true 17 | }, passportGeneralCallback)) 18 | 19 | dropboxAuth.get('/auth/dropbox', function (req, res, next) { 20 | setReturnToFromReferer(req) 21 | passport.authenticate('dropbox-oauth2')(req, res, next) 22 | }) 23 | 24 | // dropbox auth callback 25 | dropboxAuth.get('/auth/dropbox/callback', 26 | passport.authenticate('dropbox-oauth2', { 27 | successReturnToOrRedirect: config.serverURL + '/', 28 | failureRedirect: config.serverURL + '/' 29 | }) 30 | ) 31 | -------------------------------------------------------------------------------- /lib/homepage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const config = require('../config') 6 | const { User } = require('../models') 7 | const logger = require('../logger') 8 | 9 | exports.showIndex = async (req, res) => { 10 | const isLogin = req.isAuthenticated() 11 | const deleteToken = '' 12 | 13 | const data = { 14 | signin: isLogin, 15 | infoMessage: req.flash('info'), 16 | errorMessage: req.flash('error'), 17 | privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), 18 | termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')), 19 | deleteToken: deleteToken, 20 | csrfToken: req.csrfToken() 21 | } 22 | 23 | if (!isLogin) { 24 | return res.render('index.ejs', data) 25 | } 26 | 27 | const user = await User.findOne({ 28 | where: { 29 | id: req.user.id 30 | } 31 | }) 32 | if (user) { 33 | data.deleteToken = user.deleteToken 34 | return res.render('index.ejs', data) 35 | } 36 | 37 | logger.error(`error: user not found with id ${req.user.id}`) 38 | return res.render('index.ejs', data) 39 | } 40 | -------------------------------------------------------------------------------- /lib/realtime/realtimeSaveRevisionJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const models = require('../models') 4 | const logger = require('../logger') 5 | 6 | /** 7 | * clean when user not in any rooms or user not in connected list 8 | */ 9 | class SaveRevisionJob { 10 | constructor (realtime) { 11 | this.realtime = realtime 12 | this.saverSleep = false 13 | } 14 | 15 | start () { 16 | if (this.timer) return 17 | this.timer = setInterval(this.saveRevision.bind(this), 5 * 60 * 1000) 18 | } 19 | 20 | stop () { 21 | if (!this.timer) return 22 | clearInterval(this.timer) 23 | this.timer = undefined 24 | } 25 | 26 | saveRevision () { 27 | if (this.getSaverSleep()) return 28 | models.Revision.saveAllNotesRevision((err, notes) => { 29 | if (err) return logger.error('revision saver failed: ' + err) 30 | if (notes && notes.length <= 0) { 31 | this.setSaverSleep(true) 32 | } 33 | }) 34 | } 35 | 36 | getSaverSleep () { 37 | return this.saverSleep 38 | } 39 | 40 | setSaverSleep (val) { 41 | this.saverSleep = val 42 | } 43 | } 44 | 45 | exports.SaveRevisionJob = SaveRevisionJob 46 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const bodyParser = require('body-parser') 5 | const mime = require('mime-types') 6 | 7 | exports.isSQLite = function isSQLite (sequelize) { 8 | return sequelize.options.dialect === 'sqlite' 9 | } 10 | 11 | exports.getImageMimeType = function getImageMimeType (imagePath) { 12 | return mime.lookup(path.extname(imagePath)) 13 | } 14 | 15 | exports.isRevealTheme = function isRevealTheme (theme) { 16 | if (fs.existsSync(path.join(__dirname, '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) { 17 | return theme 18 | } 19 | return undefined 20 | } 21 | 22 | exports.wrap = innerHandler => (req, res, next) => innerHandler(req, res).catch(err => next(err)) 23 | 24 | // create application/x-www-form-urlencoded parser 25 | exports.urlencodedParser = bodyParser.urlencoded({ 26 | extended: false, 27 | limit: 1024 * 1024 * 10 // 10 mb 28 | }) 29 | 30 | // create text/markdown parser 31 | exports.markdownParser = bodyParser.text({ 32 | inflate: true, 33 | type: ['text/plain', 'text/markdown'], 34 | limit: 1024 * 1024 * 10 // 10 mb 35 | }) 36 | -------------------------------------------------------------------------------- /deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | database: 4 | image: postgres:11.6-alpine 5 | environment: 6 | - POSTGRES_USER=codimd 7 | - POSTGRES_PASSWORD=change_password 8 | - POSTGRES_DB=codimd 9 | volumes: 10 | - "database-data:/var/lib/postgresql/data" 11 | restart: always 12 | codimd: 13 | # you can use image or custom build below, 14 | image: nabo.codimd.dev/hackmdio/hackmd:2.5.3 15 | # Using the following command to trigger the build 16 | # docker-compose -f deployments/docker-compose.yml up --build 17 | # build: 18 | # context: .. 19 | # dockerfile: ./deployments/Dockerfile 20 | # args: 21 | # RUNTIME: hackmdio/runtime:16.20.2-35fe7e39 22 | # BUILDPACK: hackmdio/buildpack:16.20.2-35fe7e39 23 | environment: 24 | - CMD_DB_URL=postgres://codimd:change_password@database/codimd 25 | - CMD_USECDN=false 26 | depends_on: 27 | - database 28 | ports: 29 | - "3000:3000" 30 | volumes: 31 | - upload-data:/home/hackmd/app/public/uploads 32 | restart: always 33 | volumes: 34 | database-data: {} 35 | upload-data: {} 36 | -------------------------------------------------------------------------------- /lib/auth/google/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | var GoogleStrategy = require('passport-google-oauth20').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const googleAuth = module.exports = Router() 10 | 11 | passport.use(new GoogleStrategy({ 12 | clientID: config.google.clientID, 13 | clientSecret: config.google.clientSecret, 14 | callbackURL: config.serverURL + '/auth/google/callback', 15 | userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo', 16 | state: true 17 | }, passportGeneralCallback)) 18 | 19 | googleAuth.get('/auth/google', function (req, res, next) { 20 | setReturnToFromReferer(req) 21 | passport.authenticate('google', { 22 | scope: ['profile'], 23 | hostedDomain: config.google.hostedDomain 24 | })(req, res, next) 25 | }) 26 | // google auth callback 27 | googleAuth.get('/auth/google/callback', 28 | passport.authenticate('google', { 29 | successReturnToOrRedirect: config.serverURL + '/', 30 | failureRedirect: config.serverURL + '/' 31 | }) 32 | ) 33 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/auth/oauth2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | const { OAuth2CustomStrategy } = require('./strategy') 9 | 10 | const oauth2Auth = module.exports = Router() 11 | 12 | passport.use(new OAuth2CustomStrategy({ 13 | authorizationURL: config.oauth2.authorizationURL, 14 | tokenURL: config.oauth2.tokenURL, 15 | clientID: config.oauth2.clientID, 16 | clientSecret: config.oauth2.clientSecret, 17 | callbackURL: config.serverURL + '/auth/oauth2/callback', 18 | userProfileURL: config.oauth2.userProfileURL, 19 | state: config.oauth2.state, 20 | scope: config.oauth2.scope 21 | }, passportGeneralCallback)) 22 | 23 | oauth2Auth.get('/auth/oauth2', function (req, res, next) { 24 | setReturnToFromReferer(req) 25 | passport.authenticate('oauth2')(req, res, next) 26 | }) 27 | 28 | // github auth callback 29 | oauth2Auth.get('/auth/oauth2/callback', 30 | passport.authenticate('oauth2', { 31 | successReturnToOrRedirect: config.serverURL + '/', 32 | failureRedirect: config.serverURL + '/' 33 | }) 34 | ) 35 | -------------------------------------------------------------------------------- /public/js/lib/renderer/csvpreview.js: -------------------------------------------------------------------------------- 1 | import Papa from 'papaparse' 2 | import escapeHTML from 'lodash/escape' 3 | 4 | const safeParse = d => { 5 | try { 6 | return JSON.parse(d) 7 | } catch (err) { 8 | return d 9 | } 10 | } 11 | 12 | export function renderCSVPreview (csv, options = {}, attr = '') { 13 | const opt = Object.keys(options).reduce((acc, key) => { 14 | return Object.assign(acc, { 15 | [key]: safeParse(options[key]) 16 | }) 17 | }, {}) 18 | 19 | const results = Papa.parse(csv.trim(), opt) 20 | 21 | if (opt.header) { 22 | const fields = results.meta.fields 23 | return ` 24 | 25 | 26 | ${fields.map(f => ``).join('')} 27 | 28 | 29 | 30 | ${results.data.map(d => ` 31 | ${fields.map(f => ``).join('')} 32 | `).join('')} 33 | 34 |
${escapeHTML(f)}
${escapeHTML(d[f])}
` 35 | } else { 36 | return ` 37 | 38 | ${results.data.map(d => ` 39 | ${d.map(f => ``).join('')} 40 | `).join('')} 41 | 42 |
${escapeHTML(f)}
` 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/migrations/20161201050312-support-email-signin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () { 5 | return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) { 6 | if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') { 7 | console.log('Migration has already run… ignoring.') 8 | } else { 9 | throw error 10 | } 11 | }) 12 | }).catch(function (error) { 13 | 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') { 14 | console.log('Migration has already run… ignoring.') 15 | } else { 16 | throw error 17 | } 18 | }) 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | return queryInterface.removeColumn('Users', 'email').then(function () { 23 | return queryInterface.removeColumn('Users', 'password') 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/imageRouter/azure.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | const azure = require('azure-storage') 8 | 9 | exports.uploadImage = function (imagePath, callback) { 10 | if (!imagePath || typeof imagePath !== 'string') { 11 | callback(new Error('Image path is missing or wrong'), null) 12 | return 13 | } 14 | 15 | if (!callback || typeof callback !== 'function') { 16 | logger.error('Callback has to be a function') 17 | return 18 | } 19 | 20 | var azureBlobService = azure.createBlobService(config.azure.connectionString) 21 | 22 | azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) { 23 | if (err) { 24 | callback(new Error(err.message), null) 25 | } else { 26 | azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) { 27 | if (err) { 28 | callback(new Error(err.message), null) 29 | } else { 30 | callback(null, azureBlobService.getUrl(config.azure.container, result.name)) 31 | } 32 | }) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /public/js/lib/common/metrics.ejs: -------------------------------------------------------------------------------- 1 | # HELP online_notes Number of currently used notes 2 | # TYPE online_notes gauge 3 | online_notes <%- onlineNotes %> 4 | # HELP online_users Number of online users 5 | # TYPE online_users gauge 6 | online_users <%- onlineUsers %> 7 | # HELP distinct_online_users Number of distinct online users 8 | # TYPE distinct_online_users gauge 9 | distinct_online_users <%- distinctOnlineUsers %> 10 | # HELP notes_count Total count of notes 11 | # TYPE notes_count gauge 12 | notes_count <%- notesCount %> 13 | # HELP registered_users Number of registered users 14 | # TYPE registered_users gauge 15 | registered_users <%- registeredUsers %> 16 | # HELP online_registered_users Number of online registered users 17 | # TYPE online_registered_users gauge 18 | online_registered_users <%- onlineRegisteredUsers %> 19 | # HELP distinct_online_registered_users Number of distinct online registered users 20 | # TYPE distinct_online_registered_users gauge 21 | distinct_online_registered_users <%- distinctOnlineRegisteredUsers %> 22 | is_connection_busy <%- isConnectionBusy ? 1 : 0 %> 23 | connection_socket_queue_length <%- connectionSocketQueueLength %> 24 | is_disconnect_busy <%- isDisconnectBusy ? 1: 0 %> 25 | disconnect_socket_queue_length <%- disconnectSocketQueueLength %> 26 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_horiz_5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/migrations/20160607060246-support-revision.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE).then(function () { 5 | return queryInterface.createTable('Revisions', { 6 | id: { 7 | type: Sequelize.UUID, 8 | primaryKey: true 9 | }, 10 | noteId: Sequelize.UUID, 11 | patch: Sequelize.TEXT, 12 | lastContent: Sequelize.TEXT, 13 | content: Sequelize.TEXT, 14 | length: Sequelize.INTEGER, 15 | createdAt: Sequelize.DATE, 16 | updatedAt: Sequelize.DATE 17 | }) 18 | }).catch(function (error) { 19 | 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') { 20 | console.log('Migration has already run… ignoring.') 21 | } else { 22 | throw error 23 | } 24 | }) 25 | }, 26 | 27 | down: function (queryInterface, Sequelize) { 28 | return queryInterface.dropTable('Revisions').then(function () { 29 | return queryInterface.removeColumn('Notes', 'savedAt') 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Release PR Checks 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | check-release-pr: 11 | if: startsWith(github.head_ref, 'release/') 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Check for release-notes updates 21 | run: | 22 | if ! git diff --exit-code origin/develop -- public/docs/release-notes.md; then 23 | echo "Release notes updated." 24 | else 25 | echo "Error: Release notes not updated in the PR." 26 | exit 1 27 | fi 28 | 29 | - name: Compare package.json version with master 30 | run: | 31 | git fetch origin master 32 | MASTER_PACKAGE_VERSION=$(git show origin/master:package.json | jq -r '.version') 33 | BRANCH_PACKAGE_VERSION=$(jq -r '.version' package.json) 34 | 35 | if [ "$BRANCH_PACKAGE_VERSION" != "$MASTER_PACKAGE_VERSION" ]; then 36 | echo "Version bumped in package.json." 37 | else 38 | echo "Error: Version in package.json has not been bumped." 39 | exit 1 40 | fi 41 | 42 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_horiz_6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | \ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/css/slide-preview.css: -------------------------------------------------------------------------------- 1 | .markdown-body.slides { 2 | position: relative; 3 | z-index: 1; 4 | color: #222; 5 | } 6 | 7 | .markdown-body.slides::before { 8 | content: ''; 9 | display: block; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | z-index: -1; 16 | background-color: currentColor; 17 | box-shadow: 0 0 0 50vw; 18 | } 19 | 20 | .markdown-body.slides section[data-markdown] { 21 | position: relative; 22 | margin-bottom: 1.5em; 23 | background-color: #fff; 24 | text-align: center; 25 | } 26 | 27 | .markdown-body.slides section[data-markdown] code { 28 | text-align: left; 29 | } 30 | 31 | .markdown-body.slides section[data-markdown]::before { 32 | content: ''; 33 | display: block; 34 | padding-bottom: 56.23%; 35 | } 36 | 37 | .markdown-body.slides section[data-markdown] > div:first-child { 38 | position: absolute; 39 | top: 50%; 40 | left: 1em; 41 | right: 1em; 42 | transform: translateY(-50%); 43 | max-height: 100%; 44 | overflow: hidden; 45 | } 46 | 47 | .markdown-body.slides section[data-markdown] > ul { 48 | display: inline-block; 49 | } 50 | 51 | .markdown-body.slides > section > section + section::after { 52 | content: ''; 53 | position: absolute; 54 | top: -1.5em; 55 | right: 1em; 56 | height: 1.5em; 57 | border: 3px solid #777; 58 | } 59 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_horiz_7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/migrations/20160703062241-support-authorship.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT).then(function () { 5 | return queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT) 6 | }).then(function () { 7 | return queryInterface.createTable('Authors', { 8 | id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | color: Sequelize.STRING, 14 | noteId: Sequelize.UUID, 15 | userId: Sequelize.UUID, 16 | createdAt: Sequelize.DATE, 17 | updatedAt: Sequelize.DATE 18 | }) 19 | }).catch(function (error) { 20 | 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') { 21 | console.log('Migration has already run… ignoring.') 22 | } else { 23 | throw error 24 | } 25 | }) 26 | }, 27 | 28 | down: function (queryInterface, Sequelize) { 29 | return queryInterface.dropTable('Authors').then(function () { 30 | return queryInterface.removeColumn('Revisions', 'authorship') 31 | }).then(function () { 32 | return queryInterface.removeColumn('Notes', 'authorship') 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/realtime/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const path = require('path') 5 | 6 | function makeMockSocket (headers, query) { 7 | const broadCastChannelCache = {} 8 | return { 9 | id: Math.round(Math.random() * 10000), 10 | request: { 11 | user: {} 12 | }, 13 | handshake: { 14 | headers: Object.assign({}, headers), 15 | query: Object.assign({}, query) 16 | }, 17 | on: sinon.fake(), 18 | emit: sinon.fake(), 19 | broadCastChannelCache: {}, 20 | broadcast: { 21 | to: (channel) => { 22 | if (!broadCastChannelCache[channel]) { 23 | broadCastChannelCache[channel] = { 24 | channel: channel, 25 | emit: sinon.fake() 26 | } 27 | } 28 | return broadCastChannelCache[channel] 29 | } 30 | }, 31 | disconnect: sinon.fake(), 32 | rooms: [] 33 | } 34 | } 35 | 36 | function removeModuleFromRequireCache (modulePath) { 37 | delete require.cache[require.resolve(modulePath)] 38 | } 39 | function removeLibModuleCache () { 40 | const libPath = path.resolve(path.join(__dirname, '../../lib')) 41 | Object.keys(require.cache).forEach(key => { 42 | if (key.startsWith(libPath)) { 43 | delete require.cache[require.resolve(key)] 44 | } 45 | }) 46 | } 47 | 48 | exports.makeMockSocket = makeMockSocket 49 | exports.removeModuleFromRequireCache = removeModuleFromRequireCache 50 | exports.removeLibModuleCache = removeLibModuleCache 51 | -------------------------------------------------------------------------------- /lib/realtime/realtimeCleanDanglingUserJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const async = require('async') 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | /** 8 | * clean when user not in any rooms or user not in connected list 9 | */ 10 | class CleanDanglingUserJob { 11 | constructor (realtime) { 12 | this.realtime = realtime 13 | } 14 | 15 | start () { 16 | if (this.timer) return 17 | this.timer = setInterval(this.cleanDanglingUser.bind(this), 60000) 18 | } 19 | 20 | stop () { 21 | if (!this.timer) return 22 | clearInterval(this.timer) 23 | this.timer = undefined 24 | } 25 | 26 | cleanDanglingUser () { 27 | const users = this.realtime.getUserPool() 28 | async.each(Object.keys(users), (key, callback) => { 29 | const socket = this.realtime.io.sockets.connected[key] 30 | if ((!socket && users[key]) || 31 | (socket && (!socket.rooms || socket.rooms.length <= 0))) { 32 | if (config.debug) { 33 | logger.info('cleaner found redundant user: ' + key) 34 | } 35 | if (!socket) { 36 | return callback(null, null) 37 | } 38 | if (!this.realtime.disconnectProcessQueue.checkTaskIsInQueue(socket.id)) { 39 | this.realtime.queueForDisconnect(socket) 40 | } 41 | } 42 | return callback(null, null) 43 | }, function (err) { 44 | if (err) return logger.error('cleaner error', err) 45 | }) 46 | } 47 | } 48 | 49 | exports.CleanDanglingUserJob = CleanDanglingUserJob 50 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_9.svg: -------------------------------------------------------------------------------- 1 | \ 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /contribute/developer-certificate-of-origin: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | Developer's Certificate of Origin 1.1 12 | 13 | By making a contribution to this project, I certify that: 14 | 15 | (a) The contribution was created in whole or in part by me and I 16 | have the right to submit it under the open source license 17 | indicated in the file; or 18 | 19 | (b) The contribution is based upon previous work that, to the best 20 | of my knowledge, is covered under an appropriate open source 21 | license and I have the right under that license to submit that 22 | work with modifications, whether created in whole or in part 23 | by me, under the same open source license (unless I am 24 | permitted to submit under a different license), as indicated 25 | in the file; or 26 | 27 | (c) The contribution was provided directly to me by some other 28 | person who certified (a), (b) or (c) and I have not modified 29 | it. 30 | 31 | (d) I understand and agree that this project and the contribution 32 | are public and that a record of the contribution (including all 33 | personal information I submit with it, including my sign-off) is 34 | maintained indefinitely and may be redistributed consistent with 35 | this project or the open source license(s) involved. 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Test and Build' 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test-and-build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [16.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | # from https://stackoverflow.com/a/69649733 19 | - name: Reconfigure git to use HTTP authentication 20 | run: > 21 | git config --global url."https://github.com/".insteadOf 22 | ssh://git@github.com/ 23 | 24 | - uses: actions/cache@v4 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-node- 30 | 31 | - uses: actions/setup-node@v4 32 | name: Use Node.js ${{ matrix.node-version }} 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | check-latest: true 36 | 37 | - run: npm ci 38 | - run: npm run test:ci 39 | - run: npm run build 40 | 41 | doctoc: 42 | runs-on: ubuntu-latest 43 | if: github.ref == 'refs/heads/master' || github.event.pull_request 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-node@v4 48 | name: Use Node.js 14 49 | with: 50 | node-version: 14 51 | check-latest: true 52 | - name: Install doctoc-check 53 | run: | 54 | npm install -g doctoc 55 | cp README.md README.md.orig 56 | npm run doctoc 57 | diff -q README.md README.md.orig 58 | -------------------------------------------------------------------------------- /lib/auth/gitlab/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const GitlabStrategy = require('passport-gitlab2').Strategy 6 | const config = require('../../config') 7 | const response = require('../../response') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | const HttpsProxyAgent = require('https-proxy-agent') 10 | 11 | const gitlabAuth = module.exports = Router() 12 | 13 | const gitlabAuthStrategy = new GitlabStrategy({ 14 | baseURL: config.gitlab.baseURL, 15 | clientID: config.gitlab.clientID, 16 | clientSecret: config.gitlab.clientSecret, 17 | scope: config.gitlab.scope, 18 | callbackURL: config.serverURL + '/auth/gitlab/callback', 19 | state: true 20 | }, passportGeneralCallback) 21 | 22 | if (process.env.https_proxy) { 23 | const httpsProxyAgent = new HttpsProxyAgent(process.env.https_proxy) 24 | gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent) 25 | } 26 | 27 | passport.use(gitlabAuthStrategy) 28 | 29 | gitlabAuth.get('/auth/gitlab', function (req, res, next) { 30 | setReturnToFromReferer(req) 31 | passport.authenticate('gitlab')(req, res, next) 32 | }) 33 | 34 | // gitlab auth callback 35 | gitlabAuth.get('/auth/gitlab/callback', 36 | passport.authenticate('gitlab', { 37 | successReturnToOrRedirect: config.serverURL + '/', 38 | failureRedirect: config.serverURL + '/' 39 | }) 40 | ) 41 | 42 | if (!config.gitlab.scope || config.gitlab.scope === 'api') { 43 | // gitlab callback actions 44 | gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | args: 9 | VARIANT: 14-bullseye 10 | environment: 11 | - CMD_DB_URL=postgres://codimd:codimd@localhost/codimd 12 | - CMD_USECDN=false 13 | volumes: 14 | - ..:/workspace:cached 15 | - node_modules:/workspace/node_modules:cached 16 | 17 | # Overrides default command so things don't shut down after the process ends. 18 | command: sleep infinity 19 | 20 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 21 | network_mode: service:db 22 | 23 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 24 | 25 | # Uncomment the next line to use a non-root user for all processes. 26 | # user: vscode 27 | 28 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 29 | # (Adding the "ports" property to this file will not forward from a Codespace.) 30 | 31 | db: 32 | image: postgres:12.7-alpine 33 | restart: unless-stopped 34 | volumes: 35 | - postgres-data:/var/lib/postgresql/data 36 | environment: 37 | - POSTGRES_USER=codimd 38 | - POSTGRES_PASSWORD=codimd 39 | - POSTGRES_DB=codimd 40 | 41 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. 42 | # (Adding the "ports" property to this file will not forward from a Codespace.) 43 | 44 | volumes: 45 | node_modules: 46 | postgres-data: -------------------------------------------------------------------------------- /lib/imageRouter/minio.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const config = require('../config') 6 | const { getImageMimeType } = require('../utils') 7 | const logger = require('../logger') 8 | 9 | const Minio = require('minio') 10 | const minioClient = new Minio.Client({ 11 | endPoint: config.minio.endPoint, 12 | port: config.minio.port, 13 | useSSL: config.minio.secure, 14 | accessKey: config.minio.accessKey, 15 | secretKey: config.minio.secretKey 16 | }) 17 | 18 | exports.uploadImage = function (imagePath, callback) { 19 | if (!imagePath || typeof imagePath !== 'string') { 20 | callback(new Error('Image path is missing or wrong'), null) 21 | return 22 | } 23 | 24 | if (!callback || typeof callback !== 'function') { 25 | logger.error('Callback has to be a function') 26 | return 27 | } 28 | 29 | fs.readFile(imagePath, function (err, buffer) { 30 | if (err) { 31 | callback(new Error(err), null) 32 | return 33 | } 34 | 35 | const key = path.join('uploads', path.basename(imagePath)) 36 | const protocol = config.minio.secure ? 'https' : 'http' 37 | 38 | minioClient.putObject(config.s3bucket, key, buffer, buffer.size, getImageMimeType(imagePath), function (err, data) { 39 | if (err) { 40 | callback(new Error(err), null) 41 | return 42 | } 43 | const hidePort = [80, 443].includes(config.minio.port) 44 | const urlPort = hidePort ? '' : `:${config.minio.port}` 45 | callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`) 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /public/views/shared/revision-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/migrations/20150702001020-update-to-0_3_1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'shortid', { 5 | type: Sequelize.STRING, 6 | defaultValue: '0000000000', 7 | allowNull: false 8 | }).then(function () { 9 | return queryInterface.addIndex('Notes', ['shortid'], { 10 | indicesType: 'UNIQUE' 11 | }) 12 | }).then(function () { 13 | return queryInterface.addColumn('Notes', 'permission', { 14 | type: Sequelize.STRING, 15 | defaultValue: 'private', 16 | allowNull: false 17 | }) 18 | }).then(function () { 19 | return queryInterface.addColumn('Notes', 'viewcount', { 20 | type: Sequelize.INTEGER, 21 | defaultValue: 0 22 | }) 23 | }).catch(function (error) { 24 | 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') { 25 | console.log('Migration has already run… ignoring.') 26 | } else { 27 | throw error 28 | } 29 | }) 30 | }, 31 | 32 | down: function (queryInterface, Sequelize) { 33 | return queryInterface.removeColumn('Notes', 'viewcount') 34 | .then(function () { 35 | return queryInterface.removeColumn('Notes', 'permission') 36 | }) 37 | .then(function () { 38 | return queryInterface.removeIndex('Notes', ['shortid']) 39 | }) 40 | .then(function () { 41 | return queryInterface.removeColumn('Notes', 'shortid') 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/auth/mattermost/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('babel-polyfill') 3 | require('isomorphic-fetch') 4 | const Router = require('express').Router 5 | const passport = require('passport') 6 | const { Client4: MattermostClient } = require('@mattermost/client') 7 | const OAuthStrategy = require('passport-oauth2').Strategy 8 | const config = require('../../config') 9 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 10 | 11 | const mattermostAuth = module.exports = Router() 12 | 13 | const mattermostClient = new MattermostClient() 14 | 15 | const mattermostStrategy = new OAuthStrategy({ 16 | authorizationURL: config.mattermost.baseURL + '/oauth/authorize', 17 | tokenURL: config.mattermost.baseURL + '/oauth/access_token', 18 | clientID: config.mattermost.clientID, 19 | clientSecret: config.mattermost.clientSecret, 20 | callbackURL: config.serverURL + '/auth/mattermost/callback' 21 | }, passportGeneralCallback) 22 | 23 | mattermostStrategy.userProfile = (accessToken, done) => { 24 | mattermostClient.setUrl(config.mattermost.baseURL) 25 | mattermostClient.setToken(accessToken) 26 | mattermostClient.getMe() 27 | .then((data) => done(null, data)) 28 | .catch((err) => done(err)) 29 | } 30 | 31 | passport.use(mattermostStrategy) 32 | 33 | mattermostAuth.get('/auth/mattermost', function (req, res, next) { 34 | setReturnToFromReferer(req) 35 | passport.authenticate('oauth2')(req, res, next) 36 | }) 37 | 38 | // mattermost auth callback 39 | mattermostAuth.get('/auth/mattermost/callback', 40 | passport.authenticate('oauth2', { 41 | successReturnToOrRedirect: config.serverURL + '/', 42 | failureRedirect: config.serverURL + '/' 43 | }) 44 | ) 45 | -------------------------------------------------------------------------------- /public/views/index/foot.ejs: -------------------------------------------------------------------------------- 1 | <% if(useCDN) { %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- include('../build/cover-scripts') %> 11 | <% } else { %> 12 | <%- include('../build/cover-pack-scripts') %> 13 | <% } %> 14 | -------------------------------------------------------------------------------- /lib/status/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const realtime = require('../realtime/realtime') 4 | const config = require('../config') 5 | 6 | exports.getStatus = async (req, res) => { 7 | res.set({ 8 | 'Cache-Control': 'private', // only cache by client 9 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 10 | 'Content-Type': 'application/json' 11 | }) 12 | 13 | try { 14 | const data = await realtime.getStatus() 15 | res.send(data) 16 | } catch (e) { 17 | console.error(e) 18 | res.status(500).send(e.toString()) 19 | } 20 | } 21 | 22 | exports.getMetrics = async (req, res) => { 23 | const data = await realtime.getStatus() 24 | 25 | res.set({ 26 | 'Cache-Control': 'private', // only cache by client 27 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 28 | 'Content-Type': 'text/plain; charset=utf-8' 29 | }) 30 | res.render('../js/lib/common/metrics.ejs', data) 31 | } 32 | 33 | exports.getConfig = (req, res) => { 34 | const data = { 35 | domain: config.domain, 36 | urlpath: config.urlPath, 37 | debug: config.debug, 38 | version: config.fullversion, 39 | plantumlServer: config.plantuml.server, 40 | DROPBOX_APP_KEY: config.dropbox.appKey, 41 | allowedUploadMimeTypes: config.allowedUploadMimeTypes, 42 | defaultUseHardbreak: config.defaultUseHardbreak, 43 | linkifyHeaderStyle: config.linkifyHeaderStyle, 44 | useCDN: config.useCDN, 45 | defaultTocDepth: config.defaultTocDepth 46 | } 47 | res.set({ 48 | 'Cache-Control': 'private', // only cache by client 49 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 50 | 'Content-Type': 'application/javascript' 51 | }) 52 | res.render('../js/lib/common/constant.ejs', data) 53 | } 54 | -------------------------------------------------------------------------------- /lib/letter-avatars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | const crypto = require('crypto') 4 | const randomcolor = require('randomcolor') 5 | const config = require('./config') 6 | 7 | // core 8 | exports.generateAvatar = function (name) { 9 | const color = randomcolor({ 10 | seed: name, 11 | luminosity: 'dark' 12 | }) 13 | const letter = name.substring(0, 1).toUpperCase() 14 | 15 | let svg = '' 16 | svg += '' 17 | svg += '' 18 | svg += '' 19 | svg += '' 20 | svg += '' + letter + '' 21 | svg += '' 22 | svg += '' 23 | svg += '' 24 | 25 | return svg 26 | } 27 | 28 | exports.generateAvatarURL = function (name, email = '', big = true) { 29 | let photo 30 | if (typeof email !== 'string') { 31 | email = '' + name + '@example.com' 32 | } 33 | name = encodeURIComponent(name) 34 | 35 | const hash = crypto.createHash('md5') 36 | hash.update(email.toLowerCase()) 37 | const hexDigest = hash.digest('hex') 38 | 39 | if (email !== '' && config.allowGravatar) { 40 | photo = 'https://www.gravatar.com/avatar/' + hexDigest 41 | if (big) { 42 | photo += '?s=400' 43 | } else { 44 | photo += '?s=96' 45 | } 46 | } else { 47 | photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || hexDigest) + '/avatar.svg' 48 | } 49 | return photo 50 | } 51 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | var fs = require('fs') 4 | var path = require('path') 5 | var Sequelize = require('sequelize') 6 | const { cloneDeep } = require('lodash') 7 | 8 | // core 9 | var config = require('../config') 10 | var logger = require('../logger') 11 | 12 | var dbconfig = cloneDeep(config.db) 13 | dbconfig.logging = config.debug ? (data) => { 14 | logger.info(data) 15 | } : false 16 | 17 | var sequelize = null 18 | 19 | // Heroku specific 20 | if (config.dbURL) { 21 | sequelize = new Sequelize(config.dbURL, dbconfig) 22 | } else { 23 | sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig) 24 | } 25 | 26 | // [Postgres] Handling NULL bytes 27 | // https://github.com/sequelize/sequelize/issues/6485 28 | function stripNullByte (value) { 29 | value = '' + value 30 | // eslint-disable-next-line no-control-regex 31 | return value ? value.replace(/\u0000/g, '') : value 32 | } 33 | sequelize.stripNullByte = stripNullByte 34 | 35 | function processData (data, _default, process) { 36 | if (data === undefined) return data 37 | else return data === null ? _default : (process ? process(data) : data) 38 | } 39 | sequelize.processData = processData 40 | 41 | var db = {} 42 | 43 | fs.readdirSync(__dirname) 44 | .filter(function (file) { 45 | return (file.indexOf('.') !== 0) && (file !== 'index.js') 46 | }) 47 | .forEach(function (file) { 48 | var model = require(path.join(__dirname, file))(sequelize, Sequelize) 49 | db[model.name] = model 50 | }) 51 | 52 | Object.keys(db).forEach(function (modelName) { 53 | if ('associate' in db[modelName]) { 54 | db[modelName].associate(db) 55 | } 56 | }) 57 | 58 | db.sequelize = sequelize 59 | db.Sequelize = Sequelize 60 | 61 | module.exports = db 62 | -------------------------------------------------------------------------------- /public/views/shared/refresh-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 32 | -------------------------------------------------------------------------------- /public/images/mattermost-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 33 | 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodiMD", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "app", 5 | "workspaceFolder": "/workspace", 6 | 7 | // Set *default* container specific settings.json values on container create. 8 | "settings": { 9 | "terminal.integrated.shell.linux": "/bin/zsh", 10 | "sqltools.connections": [{ 11 | "name": "Container Database", 12 | "driver": "PostgreSQL", 13 | "previewLimit": 50, 14 | "server": "localhost", 15 | "port": 5432, 16 | "database": "codimd", 17 | "username": "codimd", 18 | "password": "codimd" 19 | }], 20 | }, 21 | 22 | // Add the IDs of extensions you want installed when the container is created. 23 | "extensions": [ 24 | "dbaeumer.vscode-eslint", 25 | "visualstudioexptteam.vscodeintellicode", 26 | "christian-kohler.path-intellisense", 27 | "standard.vscode-standard", 28 | "mtxr.sqltools", 29 | "mtxr.sqltools-driver-pg", 30 | "eamodio.gitlens", 31 | "codestream.codestream", 32 | "github.vscode-pull-request-github", 33 | "cschleiden.vscode-github-actions", 34 | "hbenl.vscode-mocha-test-adapter", 35 | "hbenl.vscode-test-explorer" 36 | ], 37 | 38 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 39 | // "forwardPorts": [], 40 | 41 | "portsAttributes": { 42 | "3000": { 43 | "label": "CodiMD server", 44 | "onAutoForward": "notify" 45 | }, 46 | "5432": { 47 | "label": "PostgreSQL", 48 | "onAutoForward": "notify" 49 | } 50 | }, 51 | 52 | // Use 'postCreateCommand' to run commands after the container is created. 53 | // "postCreateCommand": "yarn install", 54 | "postCreateCommand": "sudo chown -R node:node node_modules && /workspace/bin/setup", 55 | 56 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 57 | "remoteUser": "node" 58 | } -------------------------------------------------------------------------------- /lib/ot/server.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | 3 | if (typeof ot === 'undefined') { 4 | var ot = {}; 5 | } 6 | 7 | ot.Server = (function (global) { 8 | 'use strict'; 9 | 10 | // Constructor. Takes the current document as a string and optionally the array 11 | // of all operations. 12 | function Server (document, operations) { 13 | this.document = document; 14 | this.operations = operations || []; 15 | } 16 | 17 | // Call this method whenever you receive an operation from a client. 18 | Server.prototype.receiveOperation = function (revision, operation) { 19 | if (revision < 0 || this.operations.length < revision) { 20 | throw new Error("operation revision not in history"); 21 | } 22 | // Find all operations that the client didn't know of when it sent the 23 | // operation ... 24 | var concurrentOperations = this.operations.slice(revision); 25 | 26 | // ... and transform the operation against all these operations ... 27 | var transform = operation.constructor.transform; 28 | for (var i = 0; i < concurrentOperations.length; i++) { 29 | operation = transform(operation, concurrentOperations[i])[0]; 30 | } 31 | 32 | // ... and apply that on the document. 33 | var newDocument = operation.apply(this.document); 34 | // ignore if exceed the max length of document 35 | if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length) 36 | return; 37 | this.document = newDocument; 38 | // Store operation in history. 39 | this.operations.push(operation); 40 | 41 | // It's the caller's responsibility to send the operation to all connected 42 | // clients and an acknowledgement to the creator. 43 | return operation; 44 | }; 45 | 46 | return Server; 47 | 48 | }(this)); 49 | 50 | if (typeof module === 'object') { 51 | module.exports = ot.Server; 52 | } -------------------------------------------------------------------------------- /public/js/lib/markdown/utils.js: -------------------------------------------------------------------------------- 1 | export function parseFenceCodeParams (lang) { 2 | const attrMatch = lang.match(/{(.*)}/) 3 | const params = {} 4 | if (attrMatch && attrMatch.length >= 2) { 5 | const attrs = attrMatch[1] 6 | const paraMatch = attrs.match(/([#.](\S+?)\s)|((\S+?)\s*=\s*("(.+?)"|'(.+?)'|\[[^\]]*\]|\{[}]*\}|(\S+)))/g) 7 | paraMatch && paraMatch.forEach(param => { 8 | param = param.trim() 9 | if (param[0] === '#') { 10 | params.id = param.slice(1) 11 | } else if (param[0] === '.') { 12 | if (!params.class) params.class = [] 13 | params.class = params.class.concat(param.slice(1)) 14 | } else { 15 | const offset = param.indexOf('=') 16 | const id = param.substring(0, offset).trim().toLowerCase() 17 | let val = param.substring(offset + 1).trim() 18 | const valStart = val[0] 19 | const valEnd = val[val.length - 1] 20 | if (['"', "'"].indexOf(valStart) !== -1 && ['"', "'"].indexOf(valEnd) !== -1 && valStart === valEnd) { 21 | val = val.substring(1, val.length - 1) 22 | } 23 | if (id === 'class') { 24 | if (params.class) params.class = [] 25 | params.class = params.class.concat(val) 26 | } else { 27 | params[id] = val 28 | } 29 | } 30 | }) 31 | } 32 | return params 33 | } 34 | 35 | export function serializeParamToAttribute (params) { 36 | if (Object.getOwnPropertyNames(params).length === 0) { 37 | return '' 38 | } else { 39 | return ` data-params="${escape(JSON.stringify(params))}"` 40 | } 41 | } 42 | 43 | /** 44 | * @param {HTMLElement} elem 45 | */ 46 | export function deserializeParamAttributeFromElement (elem) { 47 | const params = elem.getAttribute('data-params') 48 | if (params) { 49 | return JSON.parse(unescape(params)) 50 | } else { 51 | return {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/imageRouter/filesystem.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const fs = require('fs') 5 | const URL = require('url').URL 6 | const path = require('path') 7 | 8 | const config = require('../config') 9 | const logger = require('../logger') 10 | 11 | /** 12 | * generate a random filename for uploaded image 13 | */ 14 | function randomFilename () { 15 | const buf = crypto.randomBytes(16) 16 | return `upload_${buf.toString('hex')}` 17 | } 18 | 19 | /** 20 | * pick a filename not exist in filesystem 21 | * maximum attempt 5 times 22 | */ 23 | function pickFilename (defaultFilename) { 24 | let retryCounter = 5 25 | const extname = path.extname(defaultFilename) 26 | let filename = `${randomFilename()}${extname}` 27 | while (retryCounter-- > 0) { 28 | if (fs.existsSync(path.join(config.uploadsPath, filename))) { 29 | filename = `${randomFilename()}${extname}` 30 | continue 31 | } 32 | return filename 33 | } 34 | throw new Error('file exists.') 35 | } 36 | 37 | exports.uploadImage = function (imagePath, callback) { 38 | if (!imagePath || typeof imagePath !== 'string') { 39 | callback(new Error('Image path is missing or wrong'), null) 40 | return 41 | } 42 | 43 | if (!callback || typeof callback !== 'function') { 44 | logger.error('Callback has to be a function') 45 | return 46 | } 47 | 48 | let filename = path.basename(imagePath) 49 | try { 50 | filename = pickFilename(path.basename(imagePath)) 51 | } catch (e) { 52 | return callback(e, null) 53 | } 54 | 55 | try { 56 | fs.copyFileSync(imagePath, path.join(config.uploadsPath, filename)) 57 | } catch (e) { 58 | return callback(e, null) 59 | } 60 | 61 | let url 62 | try { 63 | url = (new URL(filename, config.serverURL + '/uploads/')).href 64 | } catch (e) { 65 | url = config.serverURL + '/uploads/' + filename 66 | } 67 | 68 | callback(null, url) 69 | } 70 | -------------------------------------------------------------------------------- /lib/config/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | exports.toBooleanConfig = function toBooleanConfig (configValue) { 7 | if (configValue && typeof configValue === 'string') { 8 | return (configValue === 'true') 9 | } 10 | return configValue 11 | } 12 | 13 | exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) { 14 | if (configValue && typeof configValue === 'string') { 15 | return (configValue.split(separator).map(arrayItem => arrayItem.trim())) 16 | } 17 | return fallback 18 | } 19 | 20 | exports.toIntegerConfig = function toIntegerConfig (configValue) { 21 | if (configValue && typeof configValue === 'string') { 22 | return parseInt(configValue) 23 | } 24 | return configValue 25 | } 26 | 27 | exports.getGitCommit = function getGitCommit (repodir) { 28 | if (!fs.existsSync(repodir + '/.git/HEAD')) { 29 | return undefined 30 | } 31 | let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8') 32 | if (reference.startsWith('ref: ')) { 33 | reference = reference.substr(5).replace('\n', '') 34 | reference = fs.readFileSync(path.resolve(repodir + '/.git', reference), 'utf8') 35 | } 36 | reference = reference.replace('\n', '') 37 | return reference 38 | } 39 | 40 | exports.getGitHubURL = function getGitHubURL (repo, reference) { 41 | // if it's not a github reference, we handle handle that anyway 42 | if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) { 43 | return repo 44 | } 45 | if (repo.startsWith('git@github.com') || repo.startsWith('ssh://git@github.com')) { 46 | repo = repo.replace(/^(ssh:\/\/)?git@github.com:/, 'https://github.com/') 47 | } 48 | 49 | if (repo.endsWith('.git')) { 50 | repo = repo.replace(/\.git$/, '/') 51 | } else if (!repo.endsWith('/')) { 52 | repo = repo + '/' 53 | } 54 | return repo + 'tree/' + reference 55 | } 56 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/tomorrow-night-bright.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: Tomorrow Night - Bright 4 | Author: Chris Kempson 5 | 6 | Port done by Gerard Braad 7 | 8 | */ 9 | 10 | .cm-s-tomorrow-night-bright.CodeMirror { background: #000000; color: #eaeaea; } 11 | .cm-s-tomorrow-night-bright div.CodeMirror-selected { background: #424242; } 12 | .cm-s-tomorrow-night-bright .CodeMirror-gutters { background: #000000; border-right: 0px; } 13 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker { color: #e78c45; } 14 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker-subtle { color: #777; } 15 | .cm-s-tomorrow-night-bright .CodeMirror-linenumber { color: #424242; } 16 | .cm-s-tomorrow-night-bright .CodeMirror-cursor { border-left: 1px solid #6A6A6A; } 17 | 18 | .cm-s-tomorrow-night-bright span.cm-comment { color: #d27b53; } 19 | .cm-s-tomorrow-night-bright span.cm-atom { color: #a16a94; } 20 | .cm-s-tomorrow-night-bright span.cm-number { color: #a16a94; } 21 | 22 | .cm-s-tomorrow-night-bright span.cm-property, .cm-s-tomorrow-night-bright span.cm-attribute { color: #99cc99; } 23 | .cm-s-tomorrow-night-bright span.cm-keyword { color: #d54e53; } 24 | .cm-s-tomorrow-night-bright span.cm-string { color: #e7c547; } 25 | 26 | .cm-s-tomorrow-night-bright span.cm-variable { color: #b9ca4a; } 27 | .cm-s-tomorrow-night-bright span.cm-variable-2 { color: #7aa6da; } 28 | .cm-s-tomorrow-night-bright span.cm-def { color: #e78c45; } 29 | .cm-s-tomorrow-night-bright span.cm-bracket { color: #eaeaea; } 30 | .cm-s-tomorrow-night-bright span.cm-tag { color: #d54e53; } 31 | .cm-s-tomorrow-night-bright span.cm-link { color: #a16a94; } 32 | .cm-s-tomorrow-night-bright span.cm-error { background: #d54e53; color: #6A6A6A; } 33 | 34 | .cm-s-tomorrow-night-bright .CodeMirror-activeline-background { background: #2a2a2a; } 35 | .cm-s-tomorrow-night-bright .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } 36 | -------------------------------------------------------------------------------- /lib/web/middleware/checkVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('util') 4 | 5 | const request = require('request') 6 | 7 | const logger = require('../../logger') 8 | const config = require('../../config') 9 | 10 | let lastCheckAt 11 | 12 | const VERSION_CHECK_ENDPOINT = 'https://evangelion.codimd.dev/' 13 | const CHECK_TIMEOUT = 1000 * 60 * 60 * 24 // 1 day 14 | 15 | const rp = promisify(request) 16 | 17 | exports.checkVersion = checkVersion 18 | /** 19 | * @param {Express.Application|Express.Request} ctx 20 | */ 21 | async function checkVersion (ctx) { 22 | if (lastCheckAt && (lastCheckAt + CHECK_TIMEOUT > Date.now())) { 23 | return 24 | } 25 | 26 | // update lastCheckAt whether the check would fail or not 27 | lastCheckAt = Date.now() 28 | 29 | try { 30 | const { statusCode, body: data } = await rp({ 31 | url: `${VERSION_CHECK_ENDPOINT}?v=${config.version}`, 32 | method: 'GET', 33 | json: true, 34 | timeout: 3000 35 | }) 36 | 37 | if (statusCode !== 200 || data.status === 'error') { 38 | logger.warn('Version check failed.') 39 | return 40 | } 41 | 42 | const locals = ctx.locals ? ctx.locals : ctx.app.locals 43 | 44 | locals.versionInfo.latest = data.latest 45 | locals.versionInfo.versionItem = data.latest ? null : data.versionItem 46 | 47 | if (!data.latest) { 48 | const { version, link } = data.versionItem 49 | 50 | logger.info(`Your CodiMD version is out of date! The latest version is ${version}. Please see what's new on ${link}.`) 51 | } 52 | } catch (err) { 53 | // ignore and skip version check 54 | logger.warn('Version check failed.') 55 | logger.warn(err) 56 | } 57 | } 58 | 59 | exports.versionCheckMiddleware = function (req, res, next) { 60 | checkVersion(req) 61 | .then(() => { 62 | next() 63 | }) 64 | .catch((err) => { 65 | logger.error(err) 66 | next() 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /public/js/utils.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | import base64url from 'base64url' 3 | 4 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i 5 | 6 | export function checkNoteIdValid (id) { 7 | const result = id.match(uuidRegex) 8 | return !!(result && result.length === 1) 9 | } 10 | 11 | export function encodeNoteId (id) { 12 | // remove dashes in UUID and encode in url-safe base64 13 | const str = id.replace(/-/g, '') 14 | const hexStr = Buffer.from(str, 'hex') 15 | return base64url.encode(hexStr) 16 | } 17 | 18 | export function decodeNoteId (encodedId) { 19 | // decode from url-safe base64 20 | const id = base64url.toBuffer(encodedId).toString('hex') 21 | // add dashes between the UUID string parts 22 | const idParts = [] 23 | idParts.push(id.substr(0, 8)) 24 | idParts.push(id.substr(8, 4)) 25 | idParts.push(id.substr(12, 4)) 26 | idParts.push(id.substr(16, 4)) 27 | idParts.push(id.substr(20, 12)) 28 | return idParts.join('-') 29 | } 30 | 31 | /** 32 | * sanitize url to prevent XSS 33 | * @see {@link https://github.com/braintree/sanitize-url/issues/52#issue-1593777166} 34 | * 35 | * @param {string} rawUrl 36 | * @returns {string} sanitized url 37 | */ 38 | export function sanitizeUrl (rawUrl) { 39 | try { 40 | const url = new URL(rawUrl) 41 | if (url.protocol === 'http:' || url.protocol === 'https:') { 42 | return url.toString() 43 | } 44 | 45 | throw new Error('Invalid protocol') 46 | } catch (error) { 47 | return 'about:blank' 48 | } 49 | } 50 | 51 | // Check if URL is a PDF based on Content-Type header 52 | export async function isPdfUrl (url) { 53 | try { 54 | const response = await fetch(url, { method: 'HEAD' }) 55 | const contentType = response.headers.get('Content-Type') 56 | return contentType === 'application/pdf' 57 | } catch (error) { 58 | console.warn('Error checking PDF content type:', error) 59 | return false 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/fretb_vert_15.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/views/shared/pandoc-export-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 34 | -------------------------------------------------------------------------------- /public/docs/privacy.md.example: -------------------------------------------------------------------------------- 1 | Privacy 2 | === 3 | 4 | We process the following data, for the following purposes: 5 | 6 | |your data|our usage| 7 | |---------|---------| 8 | |IP-Address|Used to communicate with your browser and our servers. It's may exposed to third-parties which provide resources for this service. These services are, depending on your login method, the document you visit and the setup of this instance: Google, Disqus, MathJax, GitHub, SlideShare/LinkedIn, yahoo, Gravatar, Imgur, Amazon, and Cloudflare.| 9 | |Usernames and profiles|Your username as well as user profiles that are connected with it are transmitted and stored by us to provide a useful login integration with services like GitHub, Facebook, Twitter, GitLab, Dropbox, Google. Depending on the setup of this CodiMD instance there are maybe other third-parties involved using SAML, LDAP or the integration with a Mattermost instance.| 10 | |Profile pictures| Your profile picture is either loaded from the service you used to login, the CodiMD instance or Gravatar.| 11 | |Uploaded pictures| Pictures that are uploaded for documents are either uploaded to Amazon S3, Imgur, a minio instance or the local filesystem of the CodiMD server.| 12 | 13 | All account data and notes are stored in a mysql/postgres/sqlite database. Besides the user accounts and the document themselves also relationships between the documents and the user accounts are stored. This includes ownership, authorship and revisions of all changes made during the creation of a note. 14 | 15 | To delete your account and all your notes owned by your user account, you can find a button in the drop down menu on the front page. 16 | 17 | The deletion of guest notes is not possible. These don't have any ownership and this means we can't connect these to you or anyone else. If you participated in a guest note or a note owned by someone else, your authorship for the revisions is removed from these notes as well. But the content you created will stay in place as the integrity of these notes has to stay untouched. 18 | -------------------------------------------------------------------------------- /lib/auth/openid/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const OpenIDStrategy = require('@passport-next/passport-openid').Strategy 6 | const config = require('../../config') 7 | const models = require('../../models') 8 | const logger = require('../../logger') 9 | const { urlencodedParser } = require('../../utils') 10 | const { setReturnToFromReferer } = require('../utils') 11 | 12 | const openIDAuth = module.exports = Router() 13 | 14 | passport.use(new OpenIDStrategy({ 15 | returnURL: config.serverURL + '/auth/openid/callback', 16 | realm: config.serverURL, 17 | profile: true 18 | }, function (openid, profile, done) { 19 | var stringifiedProfile = JSON.stringify(profile) 20 | models.User.findOrCreate({ 21 | where: { 22 | profileid: openid 23 | }, 24 | defaults: { 25 | profile: stringifiedProfile 26 | } 27 | }).spread(function (user, created) { 28 | if (user) { 29 | var needSave = false 30 | if (user.profile !== stringifiedProfile) { 31 | user.profile = stringifiedProfile 32 | needSave = true 33 | } 34 | if (needSave) { 35 | user.save().then(function () { 36 | if (config.debug) { logger.info('user login: ' + user.id) } 37 | return done(null, user) 38 | }) 39 | } else { 40 | if (config.debug) { logger.info('user login: ' + user.id) } 41 | return done(null, user) 42 | } 43 | } 44 | }).catch(function (err) { 45 | logger.error('auth callback failed: ' + err) 46 | return done(err, null) 47 | }) 48 | })) 49 | 50 | openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) { 51 | setReturnToFromReferer(req) 52 | passport.authenticate('openid')(req, res, next) 53 | }) 54 | 55 | // openID auth callback 56 | openIDAuth.get('/auth/openid/callback', 57 | passport.authenticate('openid', { 58 | successReturnToOrRedirect: config.serverURL + '/', 59 | failureRedirect: config.serverURL + '/' 60 | }) 61 | ) 62 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number7_h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/auth/oauth2/strategy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const chance = require('chance')() 6 | 7 | const { extractProfileAttribute } = require('../../../lib/auth/oauth2/strategy') 8 | 9 | describe('OAuth2CustomStrategy', function () { 10 | describe('#extractProfileAttribute', function () { 11 | const data = { 12 | user: { 13 | email: chance.email() 14 | }, 15 | arrayData: [ 16 | { 17 | email: chance.email() 18 | }, 19 | { 20 | email: chance.email() 21 | } 22 | ] 23 | } 24 | 25 | it('should parse normal attribute correctly', function () { 26 | assert(extractProfileAttribute(data, 'user.email') === data.user.email) 27 | }) 28 | 29 | it('should return undefined when nested object key not exists', function () { 30 | assert(extractProfileAttribute(data, 'user.profile') === undefined) 31 | }) 32 | 33 | it('should return undefined when whole object key not exists', function () { 34 | assert(extractProfileAttribute(data, 'profile.email') === undefined) 35 | }) 36 | 37 | it('should return attribute in array correct', function () { 38 | assert(extractProfileAttribute(data, 'arrayData[0].email') === data.arrayData[0].email) 39 | assert(extractProfileAttribute(data, 'arrayData[1].email') === data.arrayData[1].email) 40 | }) 41 | 42 | it('should return undefined when array index out of bound', function () { 43 | assert(extractProfileAttribute(data, 'arrayData[3].email') === undefined) 44 | }) 45 | 46 | it('should return undefined when array key not exists', function () { 47 | assert(extractProfileAttribute(data, 'notExistsArray[5].email') === undefined) 48 | }) 49 | 50 | it('should return undefined when data is undefined', function () { 51 | assert(extractProfileAttribute(undefined, 'email') === undefined) 52 | assert(extractProfileAttribute(null, 'email') === undefined) 53 | assert(extractProfileAttribute({}, 'email') === undefined) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /public/views/index/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | CodiMD - <%= __('Collaborative markdown notes') %> 10 | 11 | 12 | <% if(useCDN) { %> 13 | 14 | 15 | 16 | 17 | 18 | <%- include('../build/cover-header') %> 19 | <%- include('../shared/polyfill') %> 20 | <% } else { %> 21 | 22 | 23 | <%- include('../build/cover-pack-header') %> 24 | <% } %> 25 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number1_h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | 6 | const config = require('../config') 7 | const logger = require('../logger') 8 | const models = require('../models') 9 | 10 | const authRouter = module.exports = Router() 11 | 12 | // serialize and deserialize 13 | passport.serializeUser(function (user, done) { 14 | logger.info('serializeUser: ' + user.id) 15 | return done(null, user.id) 16 | }) 17 | 18 | passport.deserializeUser(function (id, done) { 19 | models.User.findOne({ 20 | where: { 21 | id: id 22 | } 23 | }).then(function (user) { 24 | // Don't die on non-existent user 25 | if (user == null) { 26 | return done(null, false, { message: 'Invalid UserID' }) 27 | } 28 | 29 | logger.info('deserializeUser: ' + user.id) 30 | return done(null, user) 31 | }).catch(function (err) { 32 | logger.error(err) 33 | return done(err, null) 34 | }) 35 | }) 36 | 37 | if (config.isFacebookEnable) authRouter.use(require('./facebook')) 38 | if (config.isTwitterEnable) authRouter.use(require('./twitter')) 39 | if (config.isGitHubEnable) authRouter.use(require('./github')) 40 | if (config.isBitbucketEnable) authRouter.use(require('./bitbucket')) 41 | if (config.isGitLabEnable) authRouter.use(require('./gitlab')) 42 | if (config.isMattermostEnable) authRouter.use(require('./mattermost')) 43 | if (config.isDropboxEnable) authRouter.use(require('./dropbox')) 44 | if (config.isGoogleEnable) authRouter.use(require('./google')) 45 | if (config.isLDAPEnable) authRouter.use(require('./ldap')) 46 | if (config.isSAMLEnable) authRouter.use(require('./saml')) 47 | if (config.isOAuth2Enable) authRouter.use(require('./oauth2')) 48 | if (config.isEmailEnable) authRouter.use(require('./email')) 49 | if (config.isOpenIDEnable) authRouter.use(require('./openid')) 50 | 51 | // logout 52 | authRouter.get('/logout', function (req, res, next) { 53 | if (config.debug && req.isAuthenticated()) { 54 | logger.debug('user logout: ' + req.user.id) 55 | } 56 | 57 | req.logout((err) => { 58 | if (err) { return next(err) } 59 | 60 | res.redirect(config.serverURL + '/') 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /lib/imageRouter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const Router = require('express').Router 6 | const formidable = require('formidable') 7 | 8 | const readChunk = require('read-chunk') 9 | const imageType = require('image-type') 10 | const mime = require('mime-types') 11 | 12 | const config = require('../config') 13 | const logger = require('../logger') 14 | const response = require('../response') 15 | 16 | const imageRouter = module.exports = Router() 17 | 18 | function checkImageValid (filepath) { 19 | try { 20 | const buffer = readChunk.sync(filepath, 0, 12) 21 | /** @type {{ ext: string, mime: string } | null} */ 22 | const mimetypeFromBuf = imageType(buffer) 23 | const mimeTypeFromExt = mime.lookup(path.extname(filepath)) 24 | 25 | return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) && 26 | mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt) 27 | } catch (err) { 28 | logger.error(err) 29 | return false 30 | } 31 | } 32 | 33 | // upload image 34 | imageRouter.post('/uploadimage', function (req, res) { 35 | var form = new formidable.IncomingForm({ 36 | keepExtensions: true 37 | }) 38 | 39 | form.parse(req, function (err, fields, files) { 40 | if (err || !files.image || !files.image.filepath) { 41 | response.errorForbidden(req, res) 42 | } else { 43 | if (config.debug) { 44 | logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) 45 | } 46 | 47 | if (!checkImageValid(files.image.filepath)) { 48 | return response.errorForbidden(req, res) 49 | } 50 | 51 | const uploadProvider = require('./' + config.imageUploadType) 52 | uploadProvider.uploadImage(files.image.filepath, function (err, url) { 53 | // remove temporary upload file, and ignore any error 54 | fs.unlink(files.image.filepath, () => {}) 55 | if (err !== null) { 56 | logger.error(err) 57 | return res.status(500).end('upload image error') 58 | } 59 | res.send({ 60 | link: url 61 | }) 62 | }) 63 | } 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/realtime/cleanDanglingUser.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const mock = require('mock-require') 6 | const sinon = require('sinon') 7 | const { removeModuleFromRequireCache, makeMockSocket } = require('./utils') 8 | 9 | describe('cleanDanglingUser', function () { 10 | let clock 11 | beforeEach(() => { 12 | clock = sinon.useFakeTimers() 13 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake')) 14 | mock('../../lib/logger', { 15 | error: () => {}, 16 | info: () => {} 17 | }) 18 | mock('../../lib/history', {}) 19 | mock('../../lib/models', { 20 | Revision: { 21 | saveAllNotesRevision: () => { 22 | } 23 | } 24 | }) 25 | mock('../../lib/config', { 26 | debug: true 27 | }) 28 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub')) 29 | mock('../../lib/realtimeSaveRevisionJob', require('../testDoubles/realtimeJobStub')) 30 | }) 31 | 32 | afterEach(() => { 33 | clock.restore() 34 | removeModuleFromRequireCache('../../lib/realtime/realtime') 35 | mock.stopAll() 36 | sinon.restore() 37 | }) 38 | 39 | it('should call queueForDisconnectSpy when user is dangling', (done) => { 40 | const realtime = require('../../lib/realtime/realtime') 41 | const queueForDisconnectSpy = sinon.spy(realtime, 'queueForDisconnect') 42 | realtime.io = { 43 | to: sinon.stub().callsFake(function () { 44 | return { 45 | emit: sinon.fake() 46 | } 47 | }), 48 | sockets: { 49 | connected: {} 50 | } 51 | } 52 | const user1Socket = makeMockSocket() 53 | const user2Socket = makeMockSocket() 54 | 55 | user1Socket.rooms.push('room1') 56 | 57 | realtime.io.sockets.connected[user1Socket.id] = user1Socket 58 | realtime.io.sockets.connected[user2Socket.id] = user2Socket 59 | 60 | realtime.users[user1Socket.id] = user1Socket 61 | realtime.users[user2Socket.id] = user2Socket 62 | clock.tick(60000) 63 | clock.restore() 64 | setTimeout(() => { 65 | assert(queueForDisconnectSpy.called) 66 | done() 67 | }, 50) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /public/markdown-lint/css/lint.css: -------------------------------------------------------------------------------- 1 | /* The lint marker gutter */ 2 | .CodeMirror-lint-markers { 3 | width: 16px; 4 | } 5 | 6 | .CodeMirror-lint-tooltip { 7 | background-color: #333333; 8 | border: 1px solid #eeeeee; 9 | border-radius: 4px; 10 | color: white; 11 | font-family: "Source Code Pro", Consolas, monaco, monospace; 12 | font-size: 10pt; 13 | overflow: hidden; 14 | padding: 2px 5px; 15 | position: fixed; 16 | white-space: pre; 17 | white-space: pre-wrap; 18 | z-index: 100; 19 | max-width: 600px; 20 | opacity: 0; 21 | transition: opacity .4s; 22 | -moz-transition: opacity .4s; 23 | -webkit-transition: opacity .4s; 24 | -o-transition: opacity .4s; 25 | -ms-transition: opacity .4s; 26 | } 27 | 28 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { 29 | background-position: left bottom; 30 | background-repeat: repeat-x; 31 | } 32 | 33 | .CodeMirror-lint-mark-error { 34 | background-image: url(../images/mark-error.png); 35 | } 36 | 37 | .CodeMirror-lint-mark-warning { 38 | background-image: url(../images/mark-warning.png); 39 | } 40 | 41 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { 42 | background-position: center center; 43 | background-repeat: no-repeat; 44 | cursor: pointer; 45 | display: inline-block; 46 | height: 16px; 47 | width: 16px; 48 | vertical-align: middle; 49 | position: relative; 50 | margin-left: 5px; 51 | } 52 | 53 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { 54 | padding-left: 20px; 55 | background-position: top left; 56 | background-repeat: no-repeat; 57 | background-position-y: 2px; 58 | } 59 | 60 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 61 | background-image: url(../images/message-error.png); 62 | } 63 | 64 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 65 | background-image: url(../images/message-warning.png); 66 | } 67 | 68 | .CodeMirror-lint-marker-multiple { 69 | background-image: url(../images/mark-multiple.png); 70 | background-repeat: no-repeat; 71 | background-position: right bottom; 72 | width: 100%; height: 100%; 73 | } 74 | 75 | .CodeMirror-hints { 76 | background: #333; 77 | } 78 | 79 | .CodeMirror-hint { 80 | color: white; 81 | } 82 | -------------------------------------------------------------------------------- /test/realtime/realtime.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env node, mocha */ 4 | 5 | const mock = require('mock-require') 6 | const assert = require('assert') 7 | 8 | describe('realtime', function () { 9 | describe('checkViewPermission', function () { 10 | // role -> guest, loggedInUser, loggedInOwner 11 | const viewPermission = { 12 | freely: [true, true, true], 13 | editable: [true, true, true], 14 | limited: [false, true, true], 15 | locked: [true, true, true], 16 | protected: [false, true, true], 17 | private: [false, false, true] 18 | } 19 | const loggedInUserId = 'user1_id' 20 | const ownerUserId = 'user2_id' 21 | const guestReq = {} 22 | const loggedInUserReq = { 23 | user: { 24 | id: loggedInUserId, 25 | logged_in: true 26 | } 27 | } 28 | const loggedInOwnerReq = { 29 | user: { 30 | id: ownerUserId, 31 | logged_in: true 32 | } 33 | } 34 | 35 | const note = { 36 | owner: ownerUserId 37 | } 38 | 39 | let realtime 40 | 41 | beforeEach(() => { 42 | mock('../../lib/logger', { 43 | error: () => { 44 | } 45 | }) 46 | mock('../../lib/history', {}) 47 | mock('../../lib/models', { 48 | Note: { 49 | parseNoteTitle: (data) => (data) 50 | } 51 | }) 52 | mock('../../lib/config', {}) 53 | realtime = require('../../lib/realtime/realtime') 54 | }) 55 | 56 | Object.keys(viewPermission).forEach(function (permission) { 57 | describe(permission, function () { 58 | beforeEach(() => { 59 | note.permission = permission 60 | }) 61 | it('guest permission test', function () { 62 | assert(realtime.checkViewPermission(guestReq, note) === viewPermission[permission][0]) 63 | }) 64 | it('loggedIn User permission test', function () { 65 | assert(realtime.checkViewPermission(loggedInUserReq, note) === viewPermission[permission][1]) 66 | }) 67 | it('loggedIn Owner permission test', function () { 68 | assert(realtime.checkViewPermission(loggedInOwnerReq, note) === viewPermission[permission][2]) 69 | }) 70 | }) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/letter-avatars.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 3 | 'use strict' 4 | 5 | const assert = require('assert') 6 | const mock = require('mock-require') 7 | 8 | describe('generateAvatarURL() gravatar enabled', function () { 9 | let avatars 10 | beforeEach(function () { 11 | // Reset config to make sure we don't influence other tests 12 | const testconfig = { 13 | allowGravatar: true, 14 | serverURL: 'http://localhost:3000', 15 | port: 3000 16 | } 17 | mock('../lib/config', testconfig) 18 | avatars = mock.reRequire('../lib/letter-avatars') 19 | }) 20 | 21 | it('should return correct urls', function () { 22 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=400') 23 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=96') 24 | }) 25 | 26 | it('should return correct urls for names with spaces', function () { 27 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 28 | }) 29 | }) 30 | 31 | describe('generateAvatarURL() gravatar disabled', function () { 32 | let avatars 33 | beforeEach(function () { 34 | // Reset config to make sure we don't influence other tests 35 | const testconfig = { 36 | allowGravatar: false, 37 | serverURL: 'http://localhost:3000', 38 | port: 3000 39 | } 40 | mock('../lib/config', testconfig) 41 | avatars = mock.reRequire('../lib/letter-avatars') 42 | }) 43 | 44 | it('should return correct urls', function () { 45 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 46 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 47 | }) 48 | 49 | it('should return correct urls for names with spaces', function () { 50 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /lib/imageRouter/s3.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const config = require('../config') 6 | const { getImageMimeType } = require('../utils') 7 | const logger = require('../logger') 8 | 9 | const { S3Client } = require('@aws-sdk/client-s3-node/S3Client') 10 | const { PutObjectCommand } = require('@aws-sdk/client-s3-node/commands/PutObjectCommand') 11 | 12 | const credentials = { 13 | accessKeyId: config.s3.accessKeyId, 14 | secretAccessKey: config.s3.secretAccessKey 15 | } 16 | 17 | const s3 = new S3Client({ 18 | credentials, 19 | region: config.s3.region, 20 | endpoint: config.s3.endpoint 21 | }) 22 | 23 | exports.uploadImage = function (imagePath, callback) { 24 | if (!imagePath || typeof imagePath !== 'string') { 25 | callback(new Error('Image path is missing or wrong'), null) 26 | return 27 | } 28 | 29 | if (!callback || typeof callback !== 'function') { 30 | logger.error('Callback has to be a function') 31 | return 32 | } 33 | 34 | fs.readFile(imagePath, function (err, buffer) { 35 | if (err) { 36 | callback(new Error(err), null) 37 | return 38 | } 39 | const params = { 40 | Bucket: config.s3bucket, 41 | Key: path.join('uploads', path.basename(imagePath)), 42 | Body: buffer, 43 | ACL: 'public-read' 44 | } 45 | const mimeType = getImageMimeType(imagePath) 46 | if (mimeType) { params.ContentType = mimeType } 47 | 48 | const command = new PutObjectCommand(params) 49 | 50 | s3.send(command).then(data => { 51 | // default scheme settings to https 52 | let s3Endpoint = 'https://s3.amazonaws.com' 53 | if (config.s3.region && config.s3.region !== 'us-east-1') { 54 | s3Endpoint = `https://s3-${config.s3.region}.amazonaws.com` 55 | } 56 | // rewrite endpoint from config 57 | if (config.s3.endpoint) { 58 | s3Endpoint = config.s3.endpoint 59 | } 60 | if (config.s3.baseURL) { 61 | callback(null, `${config.s3.baseURL}/${params.Key}`) 62 | } else { 63 | callback(null, `${s3Endpoint}/${config.s3bucket}/${params.Key}`) 64 | } 65 | }).catch(err => { 66 | if (err) { 67 | callback(new Error(err), null) 68 | } 69 | }) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /public/vendor/ot/socketio-adapter.js: -------------------------------------------------------------------------------- 1 | /*global ot */ 2 | 3 | ot.SocketIOAdapter = (function () { 4 | 'use strict'; 5 | 6 | function SocketIOAdapter(socket) { 7 | this.socket = socket; 8 | 9 | var self = this; 10 | socket.on('client_left', function (clientId) { 11 | self.trigger('client_left', clientId); 12 | }); 13 | socket.on('set_name', function (clientId, name) { 14 | self.trigger('set_name', clientId, name); 15 | }); 16 | socket.on('set_color', function (clientId, color) { 17 | self.trigger('set_color', clientId, color); 18 | }); 19 | socket.on('ack', function (revision) { 20 | self.trigger('ack', revision); 21 | }); 22 | socket.on('operation', function (clientId, revision, operation, selection) { 23 | self.trigger('operation', revision, operation); 24 | self.trigger('selection', clientId, selection); 25 | }); 26 | socket.on('operations', function (head, operations) { 27 | self.trigger('operations', head, operations); 28 | }); 29 | socket.on('selection', function (clientId, selection) { 30 | self.trigger('selection', clientId, selection); 31 | }); 32 | socket.on('reconnect', function () { 33 | self.trigger('reconnect'); 34 | }); 35 | } 36 | 37 | SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) { 38 | this.socket.emit('operation', revision, operation, selection); 39 | }; 40 | 41 | SocketIOAdapter.prototype.sendSelection = function (selection) { 42 | this.socket.emit('selection', selection); 43 | }; 44 | 45 | SocketIOAdapter.prototype.getOperations = function (base, head) { 46 | this.socket.emit('get_operations', base, head); 47 | }; 48 | 49 | SocketIOAdapter.prototype.registerCallbacks = function (cb) { 50 | this.callbacks = cb; 51 | }; 52 | 53 | SocketIOAdapter.prototype.trigger = function (event) { 54 | var args = Array.prototype.slice.call(arguments, 1); 55 | var action = this.callbacks && this.callbacks[event]; 56 | if (action) { 57 | action.apply(this, args); 58 | } 59 | }; 60 | 61 | return SocketIOAdapter; 62 | 63 | }()); -------------------------------------------------------------------------------- /lib/realtime/realtimeUpdateDirtyNoteJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | const logger = require('../logger') 5 | const moment = require('moment') 6 | 7 | class UpdateDirtyNoteJob { 8 | constructor (realtime) { 9 | this.realtime = realtime 10 | } 11 | 12 | start () { 13 | if (this.timer) return 14 | this.timer = setInterval(this.updateDirtyNotes.bind(this), 1000) 15 | } 16 | 17 | stop () { 18 | if (!this.timer) return 19 | clearInterval(this.timer) 20 | this.timer = undefined 21 | } 22 | 23 | updateDirtyNotes () { 24 | const notes = this.realtime.getNotePool() 25 | Object.keys(notes).forEach((key) => { 26 | const note = notes[key] 27 | this.updateDirtyNote(note) 28 | .catch((err) => { 29 | logger.error('updateDirtyNote: updater error', err) 30 | }) 31 | }) 32 | } 33 | 34 | async updateDirtyNote (note) { 35 | const notes = this.realtime.getNotePool() 36 | if (!note.server.isDirty) return 37 | 38 | if (config.debug) logger.info('updateDirtyNote: updater found dirty note: ' + note.id) 39 | note.server.isDirty = false 40 | 41 | try { 42 | const _note = await this.updateNoteAsync(note) 43 | // handle when note already been clean up 44 | if (!notes[note.id] || !notes[note.id].server) return 45 | 46 | if (!_note) { 47 | this.realtime.io.to(note.id).emit('info', { 48 | code: 404 49 | }) 50 | logger.error('updateDirtyNote: note not found: ', note.id) 51 | this.realtime.disconnectSocketOnNote(note) 52 | } 53 | 54 | note.updatetime = moment(_note.lastchangeAt).valueOf() 55 | this.realtime.emitCheck(note) 56 | } catch (err) { 57 | logger.error('updateDirtyNote: note not found: ', note.id) 58 | this.realtime.io.to(note.id).emit('info', { 59 | code: 404 60 | }) 61 | this.realtime.disconnectSocketOnNote(note) 62 | throw err 63 | } 64 | } 65 | 66 | updateNoteAsync (note) { 67 | return new Promise((resolve, reject) => { 68 | this.realtime.updateNote(note, (err, _note) => { 69 | if (err) { 70 | return reject(err) 71 | } 72 | return resolve(_note) 73 | }) 74 | }) 75 | } 76 | } 77 | 78 | exports.UpdateDirtyNoteJob = UpdateDirtyNoteJob 79 | -------------------------------------------------------------------------------- /lib/auth/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const models = require('../models') 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | exports.setReturnToFromReferer = function setReturnToFromReferer (req) { 8 | if (!req.session) req.session = {} 9 | 10 | var referer = req.get('referer') 11 | var nextURL 12 | if (referer) { 13 | try { 14 | var refererSearchParams = new URLSearchParams(new URL(referer).search) 15 | nextURL = refererSearchParams.get('next') 16 | } catch (err) { 17 | logger.warn(err) 18 | } 19 | } 20 | 21 | if (nextURL) { 22 | var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//') 23 | if (isRelativeNextURL) { 24 | req.session.returnTo = (new URL(nextURL, config.serverURL)).toString() 25 | } else { 26 | req.session.returnTo = config.serverURL 27 | } 28 | } else { 29 | req.session.returnTo = referer 30 | } 31 | } 32 | 33 | exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) { 34 | var stringifiedProfile = JSON.stringify(profile) 35 | models.User.findOrCreate({ 36 | where: { 37 | profileid: profile.id.toString() 38 | }, 39 | defaults: { 40 | profile: stringifiedProfile, 41 | accessToken: accessToken, 42 | refreshToken: refreshToken 43 | } 44 | }).spread(function (user, created) { 45 | if (user) { 46 | var needSave = false 47 | if (user.profile !== stringifiedProfile) { 48 | user.profile = stringifiedProfile 49 | needSave = true 50 | } 51 | if (user.accessToken !== accessToken) { 52 | user.accessToken = accessToken 53 | needSave = true 54 | } 55 | if (user.refreshToken !== refreshToken) { 56 | user.refreshToken = refreshToken 57 | needSave = true 58 | } 59 | if (needSave) { 60 | user.save().then(function () { 61 | if (config.debug) { logger.info('user login: ' + user.id) } 62 | return done(null, user) 63 | }) 64 | } else { 65 | if (config.debug) { logger.info('user login: ' + user.id) } 66 | return done(null, user) 67 | } 68 | } 69 | }).catch(function (err) { 70 | logger.error('auth callback failed: ' + err) 71 | return done(err, null) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /public/views/codimd/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | <% if(useCDN) { %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%- include('../build/index-header') %> 19 | <%- include('../shared/polyfill') %> 20 | <% } else { %> 21 | 22 | 23 | 24 | <%- include('../build/index-pack-header') %> 25 | <% } %> 26 | 27 | -------------------------------------------------------------------------------- /public/js/lib/common/login.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, jquery */ 2 | /* global Cookies */ 3 | 4 | import { serverurl } from '../config' 5 | 6 | let checkAuth = false 7 | let profile = null 8 | let lastLoginState = getLoginState() 9 | let lastUserId = getUserId() 10 | var loginStateChangeEvent = null 11 | 12 | export function setloginStateChangeEvent (func) { 13 | loginStateChangeEvent = func 14 | } 15 | 16 | export function resetCheckAuth () { 17 | checkAuth = false 18 | } 19 | 20 | export function setLoginState (bool, id) { 21 | Cookies.set('loginstate', bool, { 22 | expires: 365 23 | }) 24 | if (id) { 25 | Cookies.set('userid', id, { 26 | expires: 365 27 | }) 28 | } else { 29 | Cookies.remove('userid') 30 | } 31 | lastLoginState = bool 32 | lastUserId = id 33 | checkLoginStateChanged() 34 | } 35 | 36 | export function checkLoginStateChanged () { 37 | if (getLoginState() !== lastLoginState || getUserId() !== lastUserId) { 38 | if (loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100) 39 | return true 40 | } else { 41 | return false 42 | } 43 | } 44 | 45 | export function getLoginState () { 46 | const state = Cookies.get('loginstate') 47 | return state === 'true' || state === true 48 | } 49 | 50 | export function getUserId () { 51 | return Cookies.get('userid') 52 | } 53 | 54 | export function clearLoginState () { 55 | Cookies.remove('loginstate') 56 | } 57 | 58 | export function checkIfAuth (yesCallback, noCallback) { 59 | const cookieLoginState = getLoginState() 60 | if (checkLoginStateChanged()) checkAuth = false 61 | if (!checkAuth || typeof cookieLoginState === 'undefined') { 62 | $.get(`${serverurl}/me`) 63 | .done(data => { 64 | if (data && data.status === 'ok') { 65 | profile = data 66 | yesCallback(profile) 67 | setLoginState(true, data.id) 68 | } else { 69 | noCallback() 70 | setLoginState(false) 71 | } 72 | }) 73 | .fail(() => { 74 | noCallback() 75 | }) 76 | .always(() => { 77 | checkAuth = true 78 | }) 79 | } else if (cookieLoginState) { 80 | yesCallback(profile) 81 | } else { 82 | noCallback() 83 | } 84 | } 85 | 86 | export default { 87 | checkAuth, 88 | profile, 89 | lastLoginState, 90 | lastUserId, 91 | loginStateChangeEvent 92 | } 93 | -------------------------------------------------------------------------------- /lib/ot/wrapped-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.WrappedOperation = (function (global) { 7 | 'use strict'; 8 | 9 | // A WrappedOperation contains an operation and corresponing metadata. 10 | function WrappedOperation (operation, meta) { 11 | this.wrapped = operation; 12 | this.meta = meta; 13 | } 14 | 15 | WrappedOperation.prototype.apply = function () { 16 | return this.wrapped.apply.apply(this.wrapped, arguments); 17 | }; 18 | 19 | WrappedOperation.prototype.invert = function () { 20 | var meta = this.meta; 21 | return new WrappedOperation( 22 | this.wrapped.invert.apply(this.wrapped, arguments), 23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ? 24 | meta.invert.apply(meta, arguments) : meta 25 | ); 26 | }; 27 | 28 | // Copy all properties from source to target. 29 | function copy (source, target) { 30 | for (var key in source) { 31 | if (source.hasOwnProperty(key)) { 32 | target[key] = source[key]; 33 | } 34 | } 35 | } 36 | 37 | function composeMeta (a, b) { 38 | if (a && typeof a === 'object') { 39 | if (typeof a.compose === 'function') { return a.compose(b); } 40 | var meta = {}; 41 | copy(a, meta); 42 | copy(b, meta); 43 | return meta; 44 | } 45 | return b; 46 | } 47 | 48 | WrappedOperation.prototype.compose = function (other) { 49 | return new WrappedOperation( 50 | this.wrapped.compose(other.wrapped), 51 | composeMeta(this.meta, other.meta) 52 | ); 53 | }; 54 | 55 | function transformMeta (meta, operation) { 56 | if (meta && typeof meta === 'object') { 57 | if (typeof meta.transform === 'function') { 58 | return meta.transform(operation); 59 | } 60 | } 61 | return meta; 62 | } 63 | 64 | WrappedOperation.transform = function (a, b) { 65 | var transform = a.wrapped.constructor.transform; 66 | var pair = transform(a.wrapped, b.wrapped); 67 | return [ 68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), 69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) 70 | ]; 71 | }; 72 | 73 | return WrappedOperation; 74 | 75 | }(this)); 76 | 77 | // Export for CommonJS 78 | if (typeof module === 'object') { 79 | module.exports = ot.WrappedOperation; 80 | } -------------------------------------------------------------------------------- /public/vendor/ot/wrapped-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.WrappedOperation = (function (global) { 7 | 'use strict'; 8 | 9 | // A WrappedOperation contains an operation and corresponing metadata. 10 | function WrappedOperation (operation, meta) { 11 | this.wrapped = operation; 12 | this.meta = meta; 13 | } 14 | 15 | WrappedOperation.prototype.apply = function () { 16 | return this.wrapped.apply.apply(this.wrapped, arguments); 17 | }; 18 | 19 | WrappedOperation.prototype.invert = function () { 20 | var meta = this.meta; 21 | return new WrappedOperation( 22 | this.wrapped.invert.apply(this.wrapped, arguments), 23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ? 24 | meta.invert.apply(meta, arguments) : meta 25 | ); 26 | }; 27 | 28 | // Copy all properties from source to target. 29 | function copy (source, target) { 30 | for (var key in source) { 31 | if (source.hasOwnProperty(key)) { 32 | target[key] = source[key]; 33 | } 34 | } 35 | } 36 | 37 | function composeMeta (a, b) { 38 | if (a && typeof a === 'object') { 39 | if (typeof a.compose === 'function') { return a.compose(b); } 40 | var meta = {}; 41 | copy(a, meta); 42 | copy(b, meta); 43 | return meta; 44 | } 45 | return b; 46 | } 47 | 48 | WrappedOperation.prototype.compose = function (other) { 49 | return new WrappedOperation( 50 | this.wrapped.compose(other.wrapped), 51 | composeMeta(this.meta, other.meta) 52 | ); 53 | }; 54 | 55 | function transformMeta (meta, operation) { 56 | if (meta && typeof meta === 'object') { 57 | if (typeof meta.transform === 'function') { 58 | return meta.transform(operation); 59 | } 60 | } 61 | return meta; 62 | } 63 | 64 | WrappedOperation.transform = function (a, b) { 65 | var transform = a.wrapped.constructor.transform; 66 | var pair = transform(a.wrapped, b.wrapped); 67 | return [ 68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), 69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) 70 | ]; 71 | }; 72 | 73 | return WrappedOperation; 74 | 75 | }(this)); 76 | 77 | // Export for CommonJS 78 | if (typeof module === 'object') { 79 | module.exports = ot.WrappedOperation; 80 | } -------------------------------------------------------------------------------- /public/css/codemirror-extend/ayu-dark.css: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/dempfi/ayu */ 2 | 3 | .cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; } 4 | .cm-s-ayu-dark div.CodeMirror-selected { background: #273747; } 5 | .cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); } 6 | .cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); } 7 | .cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; } 8 | .cm-s-ayu-dark .CodeMirror-guttermarker { color: white; } 9 | .cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; } 10 | .cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; } 11 | .cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; } 12 | 13 | .cm-s-ayu-dark span.cm-comment { color: #626a73; } 14 | .cm-s-ayu-dark span.cm-atom { color: #ae81ff; } 15 | .cm-s-ayu-dark span.cm-number { color: #e6b450; } 16 | 17 | .cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; } 18 | .cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); } 19 | .cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; } 20 | .cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; } 21 | 22 | .cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; } 23 | .cm-s-ayu-dark span.cm-keyword { color: #ff8f40; } 24 | .cm-s-ayu-dark span.cm-builtin { color: #e6b450; } 25 | .cm-s-ayu-dark span.cm-string { color: #c2d94c; } 26 | 27 | .cm-s-ayu-dark span.cm-variable { color: #b3b1ad; } 28 | .cm-s-ayu-dark span.cm-variable-2 { color: #f07178; } 29 | .cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; } 30 | .cm-s-ayu-dark span.cm-type { color: #ff8f40; } 31 | .cm-s-ayu-dark span.cm-def { color: #ffee99; } 32 | .cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; } 33 | .cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); } 34 | .cm-s-ayu-dark span.cm-header { color: #c2d94c; } 35 | .cm-s-ayu-dark span.cm-link { color: #39bae6; } 36 | .cm-s-ayu-dark span.cm-error { color: #ff3333; } 37 | 38 | .cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; } 39 | .cm-s-ayu-dark .CodeMirror-matchingbracket { 40 | text-decoration: underline; 41 | color: white !important; 42 | } 43 | -------------------------------------------------------------------------------- /test/realtime/saveRevisionJob.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const mock = require('mock-require') 6 | const sinon = require('sinon') 7 | const { removeModuleFromRequireCache, removeLibModuleCache } = require('./utils') 8 | 9 | describe('save revision job', function () { 10 | let clock 11 | let mockModels 12 | let realtime 13 | beforeEach(() => { 14 | removeLibModuleCache() 15 | mockModels = { 16 | Revision: { 17 | saveAllNotesRevision: sinon.stub() 18 | } 19 | } 20 | clock = sinon.useFakeTimers() 21 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake')) 22 | mock('../../lib/logger', { 23 | error: () => {}, 24 | info: () => {} 25 | }) 26 | mock('../../lib/history', {}) 27 | mock('../../lib/models', mockModels) 28 | mock('../../lib/config', { 29 | debug: true 30 | }) 31 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub')) 32 | mock('../../lib/realtimeCleanDanglingUserJob', require('../testDoubles/realtimeJobStub')) 33 | }) 34 | 35 | afterEach(() => { 36 | clock.restore() 37 | removeModuleFromRequireCache('../../lib/realtime/realtime') 38 | removeModuleFromRequireCache('../../lib/realtime/realtimeSaveRevisionJob') 39 | mock.stopAll() 40 | sinon.restore() 41 | }) 42 | 43 | it('should execute save revision job every 5 min', (done) => { 44 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => { 45 | callback(null, []) 46 | }) 47 | realtime = require('../../lib/realtime/realtime') 48 | clock.tick(5 * 60 * 1000) 49 | clock.restore() 50 | setTimeout(() => { 51 | assert(mockModels.Revision.saveAllNotesRevision.called) 52 | assert(realtime.saveRevisionJob.getSaverSleep() === true) 53 | done() 54 | }, 50) 55 | }) 56 | 57 | it('should not set saverSleep when more than 1 note save revision', (done) => { 58 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => { 59 | callback(null, [1]) 60 | }) 61 | realtime = require('../../lib/realtime/realtime') 62 | clock.tick(5 * 60 * 1000) 63 | clock.restore() 64 | setTimeout(() => { 65 | assert(mockModels.Revision.saveAllNotesRevision.called) 66 | assert(realtime.saveRevisionJob.getSaverSleep() === false) 67 | done() 68 | }, 50) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/js/lib/renderer/fretboard/svg/number4_h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/ayu-mirage.css: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/dempfi/ayu */ 2 | 3 | .cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; } 4 | .cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; } 5 | .cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; } 6 | .cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); } 7 | .cm-s-ayu-mirage .CodeMirror-gutters { background: #1f2430; border-right: 0px; } 8 | .cm-s-ayu-mirage .CodeMirror-guttermarker { color: white; } 9 | .cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); } 10 | .cm-s-ayu-mirage .CodeMirror-linenumber { color: rgba(61, 66, 77, 99); } 11 | .cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; } 12 | 13 | .cm-s-ayu-mirage span.cm-comment { color: #5c6773; font-style:italic; } 14 | .cm-s-ayu-mirage span.cm-atom { color: #ae81ff; } 15 | .cm-s-ayu-mirage span.cm-number { color: #ffcc66; } 16 | 17 | .cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; } 18 | .cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; } 19 | .cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; } 20 | .cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; } 21 | 22 | .cm-s-ayu-mirage span.cm-property { color: #f29e74; } 23 | .cm-s-ayu-mirage span.cm-attribute { color: #ffd580; } 24 | .cm-s-ayu-mirage span.cm-keyword { color: #ffa759; } 25 | .cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; } 26 | .cm-s-ayu-mirage span.cm-string { color: #bae67e; } 27 | 28 | .cm-s-ayu-mirage span.cm-variable { color: #cbccc6; } 29 | .cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; } 30 | .cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; } 31 | .cm-s-ayu-mirage span.cm-type { color: #ffa759; } 32 | .cm-s-ayu-mirage span.cm-def { color: #ffd580; } 33 | .cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); } 34 | .cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; } 35 | .cm-s-ayu-mirage span.cm-header { color: #bae67e; } 36 | .cm-s-ayu-mirage span.cm-link { color: #5ccfe6; } 37 | .cm-s-ayu-mirage span.cm-error { color: #ff3333; } 38 | 39 | .cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; } 40 | .cm-s-ayu-mirage .CodeMirror-matchingbracket { 41 | text-decoration: underline; 42 | color: white !important; 43 | } 44 | --------------------------------------------------------------------------------