diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..8f14718 --- /dev/null +++ b/.mailmap @@ -0,0 +1,14 @@ +Max Wu Wu Cheng-Han +Max Wu Cheng-Han, Wu +Max Wu jackycute +Max Wu Wu, Cheng-Han +Max Wu jackycute + +Sheogorath Christoph (Sheogorath) Kern + +Raccoon Raccoon Li +Raccoon Raccoon + +Peter Dave Hello Peter Dave Hello + +Claudius Coenen Claudius Coenen diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b002e54..40f7355 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,30 +1,30 @@ # Contributing When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the owners of this repository before making a change. +email, or any other method with the owners of this repository before making a change. Please note we have a code of conduct, please follow it in all your interactions with the project. ## Pull Request Process 1. Ensure you signed all your commits with Developer Certificate of Origin (DCO). -2. Ensure any install or build dependencies are removed before the end of the layer when doing a +2. Ensure any install or build dependencies are removed before the end of the layer when doing a build. -3. Update the README.md with details of changes to the interface, this includes new environment +3. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 4. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you +5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. ## Contributor Code of Conduct -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting issues, -posting feature requests, updating documentation, submitting pull requests or patches, and other +As contributors and maintainers of this project, and in the interest of fostering an open and +welcoming community, we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, submitting pull requests or patches, and other activities. -We are committed to making participation in this project a harassment-free experience for everyone, -regardless of level of experience, gender, gender identity and expression, sexual orientation, +We are committed to making participation in this project a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: @@ -37,26 +37,26 @@ Examples of unacceptable behavior by participants include: permission * Other unethical or unprofessional conduct. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, -code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By -adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently -applying these principles to every aspect of managing this project. Project maintainers who do not +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, +code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By +adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. -This code of conduct applies both within project spaces and in public spaces when an individual is +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), -version 1.2.0, available at +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), +version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) ### Sign your work We use the Developer Certificate of Origin (DCO) as a additional safeguard -for the HackMD project. This is a well established and widely used +for the CodiMD project. This is a well established and widely used mechanism to assure contributors have confirmed their right to license their contribution under the project's license. Please read [contribute/developer-certificate-of-origin][dcofile]. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index d5e679c..0000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,902 +0,0 @@ -=== .babelrc -Yukai Huang -=== .editorconfig -bananaappletw -Yukai Huang -=== .gitignore -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== .sequelizerc.example -Yukai Huang -=== .travis.yml -bananaappletw -BoHong Li -Max Wu -Peter Dave Hello -=== AUTHORS -Cheng-Han, Wu -Wu Cheng-Han -=== CONTRIBUTING.md -Max Wu -=== LICENSE -Cheng-Han, Wu -jackycute -Wu Cheng-Han -=== README.md -alecdwm -bananaappletw -BoHong Li -Cheng-Han, Wu -Florian Rhiem -jackycute -Jannik Lorenz -Jason Croft -Johannes Weißl -Jun SAKATA -Laura Kyle -Max Wu -neopostmodern -NV -Sheogorath -The Gitter Badger -Wonder Chang -Wu Cheng-Han -Yukai Huang -蒼時弦也 -=== app.js -alecdwm -BoHong Li -Cheng-Han, Wu -James Stephenson -Jan Kunzmann -Jason Croft -Jordan Matelsky -knjcode -LluisArevalo -Max Wu -NV -Peter Dave Hello -Raccoon Li -robert -Sheogorath -S.Noda -Wu Cheng-Han -xnum -Yukai Huang -=== app.json -bananaappletw -Wu Cheng-Han -Yukai Huang -=== bin/heroku -bananaappletw -Yukai Huang -=== bin/setup -Sheogorath -Wu Cheng-Han -Yukai Huang -=== config.json.example -alecdwm -bananaappletw -Wu Cheng-Han -Yukai Huang -=== docs/guides/images/s3-image-upload/bucket-policy-editor.png -Yukai Huang -=== docs/guides/images/s3-image-upload/bucket-property.png -Yukai Huang -=== docs/guides/images/s3-image-upload/create-bucket.png -Yukai Huang -=== docs/guides/images/s3-image-upload/custom-policy.png -Yukai Huang -=== docs/guides/images/s3-image-upload/iam-user.png -Yukai Huang -=== docs/guides/images/s3-image-upload/review-policy.png -Yukai Huang -=== docs/guides/s3-image-upload.md -Johannes Weißl -Yukai Huang -=== lib/config/default.js -BoHong Li -=== lib/config/defaultSSL.js -BoHong Li -=== lib/config/dockerSecret.js -BoHong Li -=== lib/config/enum.js -BoHong Li -=== lib/config/environment.js -BoHong Li -Raccoon Li -=== lib/config/index.js -BoHong Li -tkykm -=== lib/config/oldEnvironment.js -BoHong Li -Raccoon Li -=== lib/config/utils.js -Raccoon Li -=== lib/history.js -BoHong Li -Wu Cheng-Han -=== lib/letter-avatars.js -alecdwm -BoHong Li -=== lib/logger.js -BoHong Li -Wu Cheng-Han -=== lib/migrations/20150504155329-create-users.js -BoHong Li -=== lib/migrations/20150508114741-create-notes.js -BoHong Li -=== lib/migrations/20150515125813-create-temp.js -BoHong Li -=== lib/migrations/20150702001020-update-to-0_3_1.js -BoHong Li -=== lib/migrations/20150915153700-change-notes-title-to-text.js -BoHong Li -=== lib/migrations/20160112220142-note-add-lastchange.js -BoHong Li -=== lib/migrations/20160420180355-note-add-alias.js -BoHong Li -=== lib/migrations/20160515114000-user-add-tokens.js -BoHong Li -Cheng-Han, Wu -=== lib/migrations/20160607060246-support-revision.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -=== lib/migrations/20160703062241-support-authorship.js -BoHong Li -Wu Cheng-Han -=== lib/migrations/20161009040430-support-delete-note.js -BoHong Li -Wu Cheng-Han -=== lib/migrations/20161201050312-support-email-signin.js -BoHong Li -Wu Cheng-Han -=== lib/models/author.js -BoHong Li -Wu Cheng-Han -=== lib/models/index.js -bananaappletw -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== lib/models/note.js -BoHong Li -Cheng-Han, Wu -NV -Wu Cheng-Han -蒼時弦也 -=== lib/models/revision.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -=== lib/models/temp.js -BoHong Li -Cheng-Han, Wu -=== lib/models/user.js -alecdwm -BoHong Li -Cheng-Han, Wu -Jason Croft -Wu Cheng-Han -=== lib/ot/client.js -Wu Cheng-Han -=== lib/ot/editor-socketio-server.js -Wu Cheng-Han -=== lib/ot/index.js -Wu Cheng-Han -=== lib/ot/selection.js -Wu Cheng-Han -=== lib/ot/server.js -Cheng-Han, Wu -Wu Cheng-Han -=== lib/ot/simple-text-operation.js -Wu Cheng-Han -=== lib/ot/text-operation.js -Wu Cheng-Han -=== lib/ot/wrapped-operation.js -Wu Cheng-Han -=== lib/realtime.js -BoHong Li -Cheng-Han, Wu -Florian Rhiem -Wu Cheng-Han -蒼時弦也 -=== lib/response.js -alecdwm -BoHong Li -butlerx -Cheng-Han, Wu -Florian Rhiem -Ikumi Shimizu <193s@users.noreply.github.com> -Jannik Lorenz -Jason Croft -Sheogorath -Wu Cheng-Han -xnum -蒼時弦也 -=== lib/utils.js -BoHong Li -butlerx -LluisArevalo -=== lib/web/auth/dropbox/index.js -BoHong Li -=== lib/web/auth/email/index.js -BoHong Li -=== lib/web/auth/facebook/index.js -BoHong Li -=== lib/web/auth/github/index.js -BoHong Li -Max Wu -=== lib/web/auth/gitlab/index.js -BoHong Li -=== lib/web/auth/google/index.js -BoHong Li -Kaiyu Shi -=== lib/web/auth/index.js -BoHong Li -=== lib/web/auth/ldap/index.js -BoHong Li -=== lib/web/auth/twitter/index.js -BoHong Li -=== lib/web/auth/utils.js -BoHong Li -=== lib/web/baseRouter.js -BoHong Li -=== lib/web/historyRouter.js -BoHong Li -=== lib/web/imageRouter.js -BoHong Li -Kotaro Yamamoto -Raccoon Li -=== lib/web/middleware/checkURIValid.js -BoHong Li -Max Wu -=== lib/web/middleware/redirectWithoutTrailingSlashes.js -BoHong Li -=== lib/web/middleware/tooBusy.js -BoHong Li -=== lib/web/noteRouter.js -BoHong Li -=== lib/web/statusRouter.js -BoHong Li -=== lib/web/userRouter.js -BoHong Li -=== lib/web/utils.js -BoHong Li -=== lib/workers/dmpWorker.js -BoHong Li -Wu Cheng-Han -=== locales/ca.json -Xavier Marques -=== locales/da.json -Patrick Andersen -=== locales/de.json -Jannik Lorenz -Philipp Zumstein -Simon Joda Stößer -=== locales/el.json -Stratos Gerakakis -Wu Cheng-Han -zachariast -=== locales/en.json -alecdwm -Wu Cheng-Han -=== locales/eo.json -James Stephenson -=== locales/es.json -Pablo Guerrero -Sergio Valverde -=== locales/fr.json -Colin Maudry -Ho33e5 -=== locales/hi.json -Paras -=== locales/hr.json -ivanorsolic -=== locales/it.json -GhiMax -=== locales/ja.json -tkqubo -=== locales/nl.json -Martijnpold -Tom Wyckhuys -=== locales/pl.json -Bartlomiej Szala -Jakub Sygnowski -=== locales/pt.json -Marcelo Alencar -Wu Cheng-Han -=== locales/ru.json -Himura Kazuto -p0v1n0m -=== locales/sv.json -Lars Karlsson -Patrick Andersen -Wu Cheng-Han -=== locales/tr.json -Ömer Erdinç Yağmurlu -=== locales/uk.json -Dmytro Kytsmen -Wu Cheng-Han -=== locales/zh.json -Wu Cheng-Han -=== package.json -alecdwm -bananaappletw -BoHong Li -Bryan Davis -Cheng-Han, Wu -Fabien Meghazi -greenkeeperio-bot -Jason Croft -Max Wu -Peter Dave Hello -Peter Dave Hello -Sheogorath -Wu Cheng-Han -xnum -Yukai Huang -=== public/apple-touch-icon.png -Peter Dave Hello -Wu Cheng-Han -=== public/css/bootstrap-social.css -Peter Dave Hello -Wu Cheng-Han -=== public/css/center.css -Wu Cheng-Han -=== public/css/cover.css -Cheng-Han, Wu -Jason Croft -Wu Cheng-Han -=== public/css/extra.css -Cheng-Han, Wu -Wu Cheng-Han -=== public/css/font.css -Yukai Huang -=== public/css/github-extract.css -Cheng-Han, Wu -Wu Cheng-Han -=== public/css/google-font.css -Yukai Huang -=== public/css/index.css -Cheng-Han, Wu -Jason Croft -Wu Cheng-Han -=== public/css/markdown.css -Cheng-Han, Wu -Wu Cheng-Han -=== public/css/mermaid.css -Cheng-Han, Wu -=== public/css/site.css -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/css/slide-preview.css -Wu Cheng-Han -=== public/css/slide.css -Wu Cheng-Han -=== public/default.md -Wu Cheng-Han -=== public/docs/features.md -Cheng-Han, Wu -Max Wu -Pablo Guerrero -Sheogorath -Wu Cheng-Han -=== public/docs/release-notes.md -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -=== public/docs/slide-example.md -butlerx -Cheng-Han, Wu -Wu Cheng-Han -=== public/docs/yaml-metadata.md -Cheng-Han, Wu -Wu Cheng-Han -=== public/favicon.png -Peter Dave Hello -Wu Cheng-Han -=== public/fonts/SourceCodePro-Black.eot -Yukai Huang -=== public/fonts/SourceCodePro-Black.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Black.woff -Yukai Huang -=== public/fonts/SourceCodePro-Bold.eot -Yukai Huang -=== public/fonts/SourceCodePro-Bold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Bold.woff -Yukai Huang -=== public/fonts/SourceCodePro-ExtraLight.eot -Yukai Huang -=== public/fonts/SourceCodePro-ExtraLight.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-ExtraLight.woff -Yukai Huang -=== public/fonts/SourceCodePro-Light.eot -Yukai Huang -=== public/fonts/SourceCodePro-Light.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Light.woff -Yukai Huang -=== public/fonts/SourceCodePro-Medium.eot -Yukai Huang -=== public/fonts/SourceCodePro-Medium.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Medium.woff -Yukai Huang -=== public/fonts/SourceCodePro-Regular.eot -Yukai Huang -=== public/fonts/SourceCodePro-Regular.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Regular.woff -Yukai Huang -=== public/fonts/SourceCodePro-Semibold.eot -Yukai Huang -=== public/fonts/SourceCodePro-Semibold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceCodePro-Semibold.woff -Yukai Huang -=== public/fonts/SourceSansPro-Black.eot -Yukai Huang -=== public/fonts/SourceSansPro-Black.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Black.woff -Yukai Huang -=== public/fonts/SourceSansPro-BlackItalic.eot -Yukai Huang -=== public/fonts/SourceSansPro-BlackItalic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-BlackItalic.woff -Yukai Huang -=== public/fonts/SourceSansPro-Bold.eot -Yukai Huang -=== public/fonts/SourceSansPro-Bold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Bold.woff -Yukai Huang -=== public/fonts/SourceSansPro-BoldItalic.eot -Yukai Huang -=== public/fonts/SourceSansPro-BoldItalic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-BoldItalic.woff -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLight.eot -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLight.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLight.woff -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLightItalic.eot -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLightItalic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-ExtraLightItalic.woff -Yukai Huang -=== public/fonts/SourceSansPro-Italic.eot -Yukai Huang -=== public/fonts/SourceSansPro-Italic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Italic.woff -Yukai Huang -=== public/fonts/SourceSansPro-Light.eot -Yukai Huang -=== public/fonts/SourceSansPro-Light.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Light.woff -Yukai Huang -=== public/fonts/SourceSansPro-LightItalic.eot -Yukai Huang -=== public/fonts/SourceSansPro-LightItalic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-LightItalic.woff -Yukai Huang -=== public/fonts/SourceSansPro-Regular.eot -Yukai Huang -=== public/fonts/SourceSansPro-Regular.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Regular.woff -Yukai Huang -=== public/fonts/SourceSansPro-Semibold.eot -Yukai Huang -=== public/fonts/SourceSansPro-Semibold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-Semibold.woff -Yukai Huang -=== public/fonts/SourceSansPro-SemiboldItalic.eot -Yukai Huang -=== public/fonts/SourceSansPro-SemiboldItalic.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSansPro-SemiboldItalic.woff -Yukai Huang -=== public/fonts/SourceSerifPro-Bold.eot -Yukai Huang -=== public/fonts/SourceSerifPro-Bold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSerifPro-Bold.woff -Yukai Huang -=== public/fonts/SourceSerifPro-Regular.eot -Yukai Huang -=== public/fonts/SourceSerifPro-Regular.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSerifPro-Regular.woff -Yukai Huang -=== public/fonts/SourceSerifPro-Semibold.eot -Yukai Huang -=== public/fonts/SourceSerifPro-Semibold.ttf -Peter Dave Hello -Yukai Huang -=== public/fonts/SourceSerifPro-Semibold.woff -Yukai Huang -=== public/hackmd-icon-1024.png -Peter Dave Hello -Wu Cheng-Han -=== public/js/cover.js -BoHong Li -Cheng-Han, Wu -Jason Croft -NV -Wu Cheng-Han -Yukai Huang -=== public/js/extra.js -BoHong Li -Cheng-Han, Wu -NV -Wu Cheng-Han -Yukai Huang -=== public/js/google-drive-picker.js -BoHong Li -Cheng-Han, Wu -Max Wu -=== public/js/google-drive-upload.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -=== public/js/history.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/js/htmlExport.js -BoHong Li -Yukai Huang -=== public/js/index.js -BoHong Li -Cheng-Han, Wu -Jason Croft -Laura Kyle -NV -Wu Cheng-Han -xnum -Yukai Huang -Zankio -蒼時弦也 -=== public/js/lib/appState.js -Yukai Huang -=== public/js/lib/common/constant.ejs -Wu Cheng-Han -=== public/js/lib/common/login.js -BoHong Li -Wu Cheng-Han -Yukai Huang -=== public/js/lib/config/index.js -BoHong Li -Wu Cheng-Han -Yukai Huang -=== public/js/lib/editor/config.js -Yukai Huang -=== public/js/lib/editor/index.js -Wu Cheng-Han -Yukai Huang -=== public/js/lib/editor/statusbar.html -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/js/lib/editor/ui-elements.js -Yukai Huang -=== public/js/lib/editor/utils.js -Yukai Huang -=== public/js/lib/modeType.js -Yukai Huang -=== public/js/lib/syncscroll.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/js/locale.js -BoHong Li -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== public/js/pretty.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/js/render.js -BoHong Li -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/js/reveal-markdown.js -BoHong Li -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -=== public/js/slide.js -BoHong Li -Max Wu -Wu Cheng-Han -Yukai Huang -=== public/screenshot.png -Wu Cheng-Han -=== public/uploads/.gitkeep -Wu Cheng-Han -Yukai Huang -=== public/vendor/abcjs_basic_3.1.1-min.js -Wu Cheng-Han -=== public/vendor/bootstrap/tooltip.min.css -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/bootstrap/tooltip.min.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/codemirror-spell-checker/en_US.aff -Cheng-Han, Wu -=== public/vendor/codemirror-spell-checker/en_US.dic -Cheng-Han, Wu -=== public/vendor/codemirror-spell-checker/spell-checker.min.css -Cheng-Han, Wu -Wu Cheng-Han -=== public/vendor/codemirror-spell-checker/spell-checker.min.js -Cheng-Han, Wu -Wu Cheng-Han -Yukai Huang -=== public/vendor/inlineAttachment/codemirror.inline-attachment.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/inlineAttachment/inline-attachment.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-textcomplete/jquery.textcomplete.js -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-icons_222222_256x240.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-icons_454545_256x240.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-icons_888888_256x240.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/jquery-ui.min.css -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/jquery-ui/jquery-ui.min.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/md-toc.js -BoHong Li -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/ajax-adapter.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/client.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/codemirror-adapter.js -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/compress.sh -Wu Cheng-Han -=== public/vendor/ot/editor-client.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/ot.min.js -Cheng-Han, Wu -Wu Cheng-Han -=== public/vendor/ot/selection.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/socketio-adapter.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/text-operation.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/undo-manager.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/ot/wrapped-operation.js -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/showup/showup.css -Peter Dave Hello -Wu Cheng-Han -=== public/vendor/showup/showup.js -Peter Dave Hello -Wu Cheng-Han -=== public/views/error.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/hackmd.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/hackmd/body.ejs -Cheng-Han, Wu -Florian Rhiem -Ian Dees -Jason Croft -Peter Dave Hello -Wu Cheng-Han -蒼時弦也 -=== public/views/hackmd/foot.ejs -Cheng-Han, Wu -Jannik Lorenz -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== public/views/hackmd/footer.ejs -Wu Cheng-Han -=== public/views/hackmd/head.ejs -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -xnum -Yukai Huang -=== public/views/hackmd/header.ejs -Cheng-Han, Wu -Jannik Lorenz -Jason Croft -Peter Dave Hello -Wu Cheng-Han -xnum -=== public/views/html.hbs -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -=== public/views/includes/header.ejs -Wu Cheng-Han -Yukai Huang -=== public/views/includes/scripts.ejs -Wu Cheng-Han -Yukai Huang -=== public/views/index.ejs -alecdwm -Cheng-Han, Wu -Florian Rhiem -James Stephenson -Jannik Lorenz -Jason Croft -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== public/views/index/body.ejs -Wu Cheng-Han -=== public/views/index/foot.ejs -Wu Cheng-Han -=== public/views/index/footer.ejs -Wu Cheng-Han -=== public/views/index/head.ejs -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -xnum -Yukai Huang -=== public/views/index/header.ejs -Wu Cheng-Han -=== public/views/pretty.ejs -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== public/views/shared/disqus.ejs -Wu Cheng-Han -=== public/views/shared/ga.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/shared/help-modal.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/shared/polyfill.ejs -Peter Dave Hello -Wu Cheng-Han -=== public/views/shared/refresh-modal.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/shared/revision-modal.ejs -Cheng-Han, Wu -Wu Cheng-Han -=== public/views/shared/signin-modal.ejs -alecdwm -Cheng-Han, Wu -Jason Croft -neopostmodern -Sheogorath -Wu Cheng-Han -=== public/views/slide.ejs -butlerx -Cheng-Han, Wu -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== tmp/.keep -Wu Cheng-Han -=== webpack.config.js -BoHong Li -Yukai Huang -=== webpack.production.js -BoHong Li -geekyd -Wu Cheng-Han -Yukai Huang -=== webpackBaseConfig.js -BoHong Li -Peter Dave Hello -Wu Cheng-Han -Yukai Huang -=== yarn.lock -BoHong Li -Christian Schuhmann -Wu Cheng-Han -Yukai Huang diff --git a/README.md b/README.md index ba04182..a9f0d4b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -HackMD Community Edition +CodiMD === [![Standard - JavaScript Style Guide][standardjs-image]][standardjs-url] [![Join the chat at https://gitter.im/hackmdio/hackmd][gitter-image]][gitter-url] -[![#HackMD on matrix.org][matrix.org-image]][matrix.org-url] +[![#CodiMD on matrix.org][matrix.org-image]][matrix.org-url] [![build status][travis-image]][travis-url] [![version][github-version-badge]][github-release-page] [![Help Contribute to Open Source][codetriage-image]][codetriage-url] [![POEditor][poeditor-image]][poeditor-url] -HackMD lets you create realtime collaborative markdown notes on all platforms. -Inspired by Hackpad, with more focus on speed and flexibility. -Still in the early stage, feel free to fork or contribute to HackMD. +CodiMD lets you create real-time collaborative markdown notes on all platforms. +Inspired by Hackpad, with more focus on speed and flexibility, and build from [HackMD](https://hackmd.io) source code. +Feel free to contribute. Thanks for using! :smile: @@ -20,13 +20,15 @@ Thanks for using! :smile: # Table of Contents +- [HackMD CE became CodiMD](#hackmd-ce-became-codimd) - [Browsers Requirement](#browsers-requirement) - [Installation](#installation) - [Getting started (Native install)](#getting-started-native-install) - [Prerequisite](#prerequisite) - [Instructions](#instructions) - [Heroku Deployment](#heroku-deployment) - - [HackMD by docker container](#hackmd-by-docker-container) + - [Kubernetes](#kubernetes) + - [CodiMD by docker container](#codimd-by-docker-container) - [Upgrade](#upgrade) - [Native setup](#native-setup) - [Configuration](#configuration) @@ -41,6 +43,16 @@ Thanks for using! :smile: +# HackMD CE became CodiMD + +CodiMD was recently renamed from its former name was HackMD. CodiMD is the free software version of HackMD. It was the original Version of HackMD. The HackMD team initiated CodiMD and provided a solid code base. Due to the need of paying bills, A fork was created and called HackMD EE, which is a SaaS (Software as a Service) product available at [hackmd.io](https://hackmd.io). + +We decided to change the name to break the confusion between HackMD and CodiMD, formally known as HackMD CE, as it never was an open core project. + +Just to more confusion: We are still friends with HackMD :heart: + +*For the whole renaming story, see the [related issue](https://github.com/hackmdio/hackmd/issues/720)* + # Browsers Requirement - ![Chrome](http://browserbadge.com/chrome/47/18px) Chrome >= 47, Chrome for Android >= 47 @@ -59,7 +71,7 @@ Thanks for using! :smile: - Node.js 6.x or up (test up to 7.5.0) - Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8` - npm (and its dependencies, especially [uWebSockets](https://github.com/uWebSockets/uWebSockets#nodejs-developers), [node-gyp](https://github.com/nodejs/node-gyp#installation)) -- For **building** HackMD we recommend to use a machine with at least **2GB** RAM +- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM ### Instructions @@ -68,18 +80,27 @@ Thanks for using! :smile: 3. Setup the configs, see more below 4. Setup environment variables which will overwrite the configs 5. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development) -6. Run the server as you like (node, forever, pm2) +6. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string + For example: `postgres://username:password@localhost:5432/codimd` +7. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema +8. Run the server as you like (node, forever, pm2) ## Heroku Deployment -You can quickly setup a sample Heroku HackMD application by clicking the button below. +You can quickly setup a sample Heroku CodiMD application by clicking the button below. -[![Deploy on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hackmdio/hackmd/tree/master) +[![Deploy on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hackmdio/codimd/tree/master) If you deploy it without the button, keep in mind to use the right buildpacks. For details check `app.json`. -## HackMD by docker container -[![Try in PWD](https://cdn.rawgit.com/play-with-docker/stacks/cff22438/assets/images/button.png)](http://play-with-docker.com?stack=https://github.com/hackmdio/docker-hackmd/raw/master/docker-compose.yml&stack_name=hackmd) +## Kubernetes + +To install use `helm install stable/hackmd`. + +For all further details, please check out the offical CodiMD [K8s helm chart](https://github.com/kubernetes/charts/tree/master/stable/hackmd). + +## CodiMD by docker container +[![Try in PWD](https://cdn.rawgit.com/play-with-docker/stacks/cff22438/assets/images/button.png)](http://play-with-docker.com?stack=https://github.com/hackmdio/docker-hackmd/raw/master/docker-compose.yml&stack_name=codimd) **Debian-based version:** @@ -91,11 +112,11 @@ If you deploy it without the button, keep in mind to use the right buildpacks. F [![alpine](https://images.microbadger.com/badges/version/hackmdio/hackmd:alpine.svg)](https://microbadger.com/images/hackmdio/hackmd:alpine "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/hackmdio/hackmd:alpine.svg)](https://microbadger.com/images/hackmdio/hackmd:alpine "Get your own image badge on microbadger.com") -The easiest way to setup HackMD using docker are using the following three commands: +The easiest way to setup CodiMD using docker are using the following three commands: ```console git clone https://github.com/hackmdio/docker-hackmd.git -cd docker-hackmd +cd docker-codimd docker-compose up ``` Read more about it in the [docker repository…](https://github.com/hackmdio/docker-hackmd) @@ -104,14 +125,14 @@ Read more about it in the [docker repository…](https://github.com/hackmdio/doc ## Native setup -If you are upgrading HackMD from an older version, follow these steps: +If you are upgrading CodiMD from an older version, follow these steps: 1. Fully stop your old server first (important) 2. `git pull` or do whatever that updates the files 3. `npm install` to update dependencies 4. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development) 5. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string - For example: `postgres://username:password@localhost:5432/hackmd` + For example: `postgres://username:password@localhost:5432/codimd` 6. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema 7. Start your whole new server! @@ -147,79 +168,84 @@ There are some config settings you need to change in the files below. | --------- | ------ | ----------- | | `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) | | `DEBUG` | `true` or `false` | set debug mode; show more logs | -| `HMD_DOMAIN` | `hackmd.io` | domain name | -| `HMD_URL_PATH` | `hackmd` | sub URL path, like `www.example.com/` | -| `HMD_PORT` | `80` | web app port | -| `HMD_ALLOW_ORIGIN` | `localhost, hackmd.io` | domain name whitelist (use comma to separate) | -| `HMD_PROTOCOL_USESSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) | -| `HMD_URL_ADDPORT` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | -| `HMD_USECDN` | `true` or `false` | set to use CDN resources or not (default is `true`) | -| `HMD_ALLOW_ANONYMOUS` | `true` or `false` | set to allow anonymous usage (default is `true`) | -| `HMD_ALLOW_ANONYMOUS_EDITS` | `true` or `false` | if `allowAnonymous` is `true`, allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) | -| `HMD_ALLOW_FREEURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | -| `HMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) | -| `HMD_DB_URL` | `mysql://localhost:3306/database` | set the database URL | -| `HMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If non is set, one will randomly generated on startup | -| `HMD_SESSION_LIFE` | `1209600000` | Session life time. (milliseconds) | -| `HMD_FACEBOOK_CLIENTID` | no example | Facebook API client id | -| `HMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret | -| `HMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key | -| `HMD_TWITTER_CONSUMERSECRET` | no example | Twitter API consumer secret | -| `HMD_GITHUB_CLIENTID` | no example | GitHub API client id | -| `HMD_GITHUB_CLIENTSECRET` | no example | GitHub API client secret | -| `HMD_GITLAB_SCOPE` | `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) | -| `HMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) | -| `HMD_GITLAB_CLIENTID` | no example | GitLab API client id | -| `HMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret | -| `HMD_MATTERMOST_BASEURL` | no example | Mattermost authentication endpoint | -| `HMD_MATTERMOST_CLIENTID` | no example | Mattermost API client id | -| `HMD_MATTERMOST_CLIENTSECRET` | no example | Mattermost API client secret | -| `HMD_DROPBOX_CLIENTID` | no example | Dropbox API client id | -| `HMD_DROPBOX_CLIENTSECRET` | no example | Dropbox API client secret | -| `HMD_GOOGLE_CLIENTID` | no example | Google API client id | -| `HMD_GOOGLE_CLIENTSECRET` | no example | Google API client secret | -| `HMD_LDAP_URL` | `ldap://example.com` | URL of LDAP server | -| `HMD_LDAP_BINDDN` | no example | bindDn for LDAP access | -| `HMD_LDAP_BINDCREDENTIALS` | no example | bindCredentials for LDAP access | -| `HMD_LDAP_SEARCHBASE` | `o=users,dc=example,dc=com` | LDAP directory to begin search from | -| `HMD_LDAP_SEARCHFILTER` | `(uid={{username}})` | LDAP filter to search with | -| `HMD_LDAP_SEARCHATTRIBUTES` | `displayName, mail` | LDAP attributes to search with (use comma to separate) | -| `HMD_LDAP_USERIDFIELD` | `uidNumber` or `uid` or `sAMAccountName` | The LDAP field which is used uniquely identify a user on HackMD | -| `HMD_LDAP_USERNAMEFIELD` | Fallback to userid | The LDAP field which is used as the username on HackMD | -| `HMD_LDAP_TLS_CA` | `server-cert.pem, root.pem` | Root CA for LDAP TLS in PEM format (use comma to separate) | -| `HMD_LDAP_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the LDAP provider | -| `HMD_SAML_IDPSSOURL` | `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](docs/guides/auth.md#saml-onelogin). | -| `HMD_SAML_IDPCERT` | `/path/to/cert.pem` | certificate file path of IdP in PEM format | -| `HMD_SAML_ISSUER` | no example | identity of the service provider (optional, default: serverurl)" | -| `HMD_SAML_IDENTIFIERFORMAT` | no example | name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) | -| `HMD_SAML_GROUPATTRIBUTE` | `memberOf` | attribute name for group list (optional) | -| `HMD_SAML_REQUIREDGROUPS` | `Hackmd-users` | group names that allowed (use vertical bar to separate) (optional) | -| `HMD_SAML_EXTERNALGROUPS` | `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) | -| `HMD_SAML_ATTRIBUTE_ID` | `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) | -| `HMD_SAML_ATTRIBUTE_USERNAME` | `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) | -| `HMD_SAML_ATTRIBUTE_EMAIL` | `mail` | attribute map for `email` (optional, default: NameID of SAML response if `HMD_SAML_IDENTIFIERFORMAT` is default) | -| `HMD_IMGUR_CLIENTID` | no example | Imgur API client id | -| `HMD_EMAIL` | `true` or `false` | set to allow email signin | -| `HMD_ALLOW_PDF_EXPORT` | `true` or `false` | Enable or disable PDF exports | -| `HMD_ALLOW_EMAIL_REGISTER` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) | -| `HMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio` or `filesystem` | Where to upload image. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md) | -| `HMD_S3_ACCESS_KEY_ID` | no example | AWS access key id | -| `HMD_S3_SECRET_ACCESS_KEY` | no example | AWS secret key | -| `HMD_S3_REGION` | `ap-northeast-1` | AWS S3 region | -| `HMD_S3_BUCKET` | no example | AWS S3 bucket name | -| `HMD_MINIO_ACCESS_KEY` | no example | Minio access key | -| `HMD_MINIO_SECRET_KEY` | no example | Minio secret key | -| `HMD_MINIO_ENDPOINT` | `minio.example.org` | Address of your Minio endpoint/instance | -| `HMD_MINIO_PORT` | `9000` | Port that is used for your Minio instance | -| `HMD_MINIO_SECURE` | `true` | If set to `true` HTTPS is used for Minio | -| `HMD_AZURE_CONNECTION_STRING` | no example | Azure Blob Storage connection string | -| `HMD_AZURE_CONTAINER` | no example | Azure Blob Storage container name (automatically created if non existent) | -| `HMD_HSTS_ENABLE` | ` true` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | -| `HMD_HSTS_INCLUDE_SUBDOMAINS` | `true` | set to include subdomains in HSTS (default is `true`) | -| `HMD_HSTS_MAX_AGE` | `31536000` | max duration in seconds to tell clients to keep HSTS status (default is a year) | -| `HMD_HSTS_PRELOAD` | `true` | whether to allow preloading of the site's HSTS status (e.g. into browsers) | -| `HMD_CSP_ENABLE` | `true` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | -| `HMD_CSP_REPORTURI` | `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | +| `CMD_DOMAIN` | `codimd.org` | domain name | +| `CMD_URL_PATH` | `codimd` | sub URL path, like `www.example.com/` | +| `CMD_HOST` | `localhost` | host to listen on | +| `CMD_PORT` | `80` | web app port | +| `CMD_PATH` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `CMD_HOST` and `CMD_PORT` are ignored) | +| `CMD_ALLOW_ORIGIN` | `localhost, codimd.org` | domain name whitelist (use comma to separate) | +| `CMD_PROTOCOL_USESSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) | +| `CMD_URL_ADDPORT` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | +| `CMD_USECDN` | `true` or `false` | set to use CDN resources or not (default is `true`) | +| `CMD_ALLOW_ANONYMOUS` | `true` or `false` | set to allow anonymous usage (default is `true`) | +| `CMD_ALLOW_ANONYMOUS_EDITS` | `true` or `false` | if `allowAnonymous` is `true`, allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) | +| `CMD_ALLOW_FREEURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | +| `CMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) | +| `CMD_DB_URL` | `mysql://localhost:3306/database` | set the database URL | +| `CMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If non is set, one will randomly generated on startup | +| `CMD_SESSION_LIFE` | `1209600000` | Session life time. (milliseconds) | +| `CMD_FACEBOOK_CLIENTID` | no example | Facebook API client id | +| `CMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret | +| `CMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key | +| `CMD_TWITTER_CONSUMERSECRET` | no example | Twitter API consumer secret | +| `CMD_GITHUB_CLIENTID` | no example | GitHub API client id | +| `CMD_GITHUB_CLIENTSECRET` | no example | GitHub API client secret | +| `CMD_GITLAB_SCOPE` | `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) | +| `CMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) | +| `CMD_GITLAB_CLIENTID` | no example | GitLab API client id | +| `CMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret | +| `CMD_MATTERMOST_BASEURL` | no example | Mattermost authentication endpoint | +| `CMD_MATTERMOST_CLIENTID` | no example | Mattermost API client id | +| `CMD_MATTERMOST_CLIENTSECRET` | no example | Mattermost API client secret | +| `CMD_DROPBOX_CLIENTID` | no example | Dropbox API client id | +| `CMD_DROPBOX_CLIENTSECRET` | no example | Dropbox API client secret | +| `CMD_GOOGLE_CLIENTID` | no example | Google API client id | +| `CMD_GOOGLE_CLIENTSECRET` | no example | Google API client secret | +| `CMD_LDAP_URL` | `ldap://example.com` | URL of LDAP server | +| `CMD_LDAP_BINDDN` | no example | bindDn for LDAP access | +| `CMD_LDAP_BINDCREDENTIALS` | no example | bindCredentials for LDAP access | +| `CMD_LDAP_SEARCHBASE` | `o=users,dc=example,dc=com` | LDAP directory to begin search from | +| `CMD_LDAP_SEARCHFILTER` | `(uid={{username}})` | LDAP filter to search with | +| `CMD_LDAP_SEARCHATTRIBUTES` | `displayName, mail` | LDAP attributes to search with (use comma to separate) | +| `CMD_LDAP_USERIDFIELD` | `uidNumber` or `uid` or `sAMAccountName` | The LDAP field which is used uniquely identify a user on CodiMD | +| `CMD_LDAP_USERNAMEFIELD` | Fallback to userid | The LDAP field which is used as the username on CodiMD | +| `CMD_LDAP_TLS_CA` | `server-cert.pem, root.pem` | Root CA for LDAP TLS in PEM format (use comma to separate) | +| `CMD_LDAP_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the LDAP provider | +| `CMD_SAML_IDPSSOURL` | `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](docs/guides/auth.md#saml-onelogin). | +| `CMD_SAML_IDPCERT` | `/path/to/cert.pem` | certificate file path of IdP in PEM format | +| `CMD_SAML_ISSUER` | no example | identity of the service provider (optional, default: serverurl)" | +| `CMD_SAML_IDENTIFIERFORMAT` | no example | name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) | +| `CMD_SAML_GROUPATTRIBUTE` | `memberOf` | attribute name for group list (optional) | +| `CMD_SAML_REQUIREDGROUPS` | `Hackmd-users` | group names that allowed (use vertical bar to separate) (optional) | +| `CMD_SAML_EXTERNALGROUPS` | `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) | +| `CMD_SAML_ATTRIBUTE_ID` | `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) | +| `CMD_SAML_ATTRIBUTE_USERNAME` | `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) | +| `CMD_SAML_ATTRIBUTE_EMAIL` | `mail` | attribute map for `email` (optional, default: NameID of SAML response if `CMD_SAML_IDENTIFIERFORMAT` is default) | +| `CMD_IMGUR_CLIENTID` | no example | Imgur API client id | +| `CMD_EMAIL` | `true` or `false` | set to allow email signin | +| `CMD_ALLOW_PDF_EXPORT` | `true` or `false` | Enable or disable PDF exports | +| `CMD_ALLOW_EMAIL_REGISTER` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) | +| `CMD_ALLOW_GRAVATAR` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance | +| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md) | +| `CMD_S3_ACCESS_KEY_ID` | no example | AWS access key id | +| `CMD_S3_SECRET_ACCESS_KEY` | no example | AWS secret key | +| `CMD_S3_REGION` | `ap-northeast-1` | AWS S3 region | +| `CMD_S3_BUCKET` | no example | AWS S3 bucket name | +| `CMD_MINIO_ACCESS_KEY` | no example | Minio access key | +| `CMD_MINIO_SECRET_KEY` | no example | Minio secret key | +| `CMD_MINIO_ENDPOINT` | `minio.example.org` | Address of your Minio endpoint/instance | +| `CMD_MINIO_PORT` | `9000` | Port that is used for your Minio instance | +| `CMD_MINIO_SECURE` | `true` | If set to `true` HTTPS is used for Minio | +| `CMD_AZURE_CONNECTION_STRING` | no example | Azure Blob Storage connection string | +| `CMD_AZURE_CONTAINER` | no example | Azure Blob Storage container name (automatically created if non existent) | +| `CMD_HSTS_ENABLE` | ` true` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | +| `CMD_HSTS_INCLUDE_SUBDOMAINS` | `true` | set to include subdomains in HSTS (default is `true`) | +| `CMD_HSTS_MAX_AGE` | `31536000` | max duration in seconds to tell clients to keep HSTS status (default is a year) | +| `CMD_HSTS_PRELOAD` | `true` | whether to allow preloading of the site's HSTS status (e.g. into browsers) | +| `CMD_CSP_ENABLE` | `true` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | +| `CMD_CSP_REPORTURI` | `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | + +***Note:** Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue to work.* ## Application settings `config.json` @@ -227,8 +253,10 @@ There are some config settings you need to change in the files below. | --------- | ------ | ----------- | | `debug` | `true` or `false` | set debug mode, show more logs | | `domain` | `localhost` | domain name | -| `urlPath` | `hackmd` | sub URL path, like `www.example.com/` | +| `urlPath` | `codimd` | sub URL path, like `www.example.com/` | +| `host` | `localhost` | host to listen on | | `port` | `80` | web app port | +| `path` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `host` and `port` are ignored) | | `allowOrigin` | `['localhost']` | domain name whitelist | | `useSSL` | `true` or `false` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) | | `hsts` | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | @@ -241,9 +269,9 @@ There are some config settings you need to change in the files below. | `allowFreeURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL | | `defaultPermission` | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | set notes default permission (only applied on signed users) | | `dbURL` | `mysql://localhost:3306/database` | set the db URL; if set, then db config (below) won't be applied | -| `db` | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) | +| `db` | `{ "dialect": "sqlite", "storage": "./db.codimd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) | | `sslKeyPath` | `./cert/client.key` | SSL key path (only need when you set `useSSL`) | -| `sslCertPath` | `./cert/hackmd_io.crt` | SSL cert path (only need when you set `useSSL`) | +| `sslCertPath` | `./cert/codimd_io.crt` | SSL cert path (only need when you set `useSSL`) | | `sslCAPath` | `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain (only need when you set `useSSL`) | | `dhParamPath` | `./cert/dhparam.pem` | SSL dhparam path (only need when you set `useSSL`) | | `tmpPath` | `./tmp/` | temp directory path | @@ -254,6 +282,7 @@ There are some config settings you need to change in the files below. | `errorPath` | `./public/views/error.ejs` | error template file path | | `prettyPath` | `./public/views/pretty.ejs` | pretty template file path | | `slidePath` | `./public/views/slide.hbs` | slide template file path | +| `uploadsPath` | `./public/uploads` | uploads directory - needs to be persistent when you use imageUploadType `filesystem` | | `sessionName` | `connect.sid` | cookie session name | | `sessionSecret` | `secret` | cookie session secret | | `sessionLife` | `14 * 24 * 60 * 60 * 1000` | cookie session life | @@ -263,7 +292,8 @@ There are some config settings you need to change in the files below. | `documentMaxLength` | `100000` | note max length | | `email` | `true` or `false` | set to allow email signin | | `allowEmailRegister` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) | -| `imageUploadType` | `imgur`(default), `s3`, `minio`, `azure` or `filesystem` | Where to upload image +| `allowGravatar` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance | +| `imageUploadType` | `imgur`, `s3`, `minio`, `azure` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md)| | `minio` | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also checkout our [Minio Image Upload Guide](docs/guides/minio-image-upload.md) | | `s3` | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) | | `s3bucket` | `YOUR_S3_BUCKET_NAME` | bucket name when `imageUploadType` is set to `s3` or `minio` | @@ -294,7 +324,7 @@ There are some config settings you need to change in the files below. ## Structure ```text -hackmd/ +codimd/ ├── tmp/ --- temporary files ├── docs/ --- document files ├── lib/ --- server libraries @@ -320,15 +350,15 @@ See more at [http://operational-transformation.github.io/](http://operational-tr [gitter-image]: https://badges.gitter.im/Join%20Chat.svg [gitter-url]: https://gitter.im/hackmdio/hackmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge -[matrix.org-image]: https://img.shields.io/badge/Matrix.org-%23HackMD@matrix.org-green.svg -[matrix.org-url]: https://riot.im/app/#/room/#hackmd:matrix.org -[travis-image]: https://travis-ci.org/hackmdio/hackmd.svg?branch=master -[travis-url]: https://travis-ci.org/hackmdio/hackmd -[github-version-badge]: https://img.shields.io/github/release/hackmdio/hackmd.svg -[github-release-page]: https://github.com/hackmdio/hackmd/releases +[matrix.org-image]: https://img.shields.io/badge/Matrix.org-%23CodiMD@matrix.org-green.svg +[matrix.org-url]: https://riot.im/app/#/room/#codimd:matrix.org +[travis-image]: https://travis-ci.org/hackmdio/codimd.svg?branch=master +[travis-url]: https://travis-ci.org/hackmdio/codimd +[github-version-badge]: https://img.shields.io/github/release/hackmdio/codimd.svg +[github-release-page]: https://github.com/hackmdio/codimd/releases [standardjs-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg [standardjs-url]: https://github.com/feross/standard -[codetriage-image]: https://www.codetriage.com/hackmdio/hackmd/badges/users.svg -[codetriage-url]: https://www.codetriage.com/hackmdio/hackmd +[codetriage-image]: https://www.codetriage.com/hackmdio/codimd/badges/users.svg +[codetriage-url]: https://www.codetriage.com/hackmdio/codimd [poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg [poeditor-url]: https://poeditor.com/join/project/1OpGjF2Jir diff --git a/app.js b/app.js index 772073b..24f0516 100644 --- a/app.js +++ b/app.js @@ -26,22 +26,6 @@ var response = require('./lib/response') var models = require('./lib/models') var csp = require('./lib/csp') -// generate front-end constants by template -var constpath = path.join(__dirname, './public/js/lib/common/constant.ejs') -var data = { - domain: config.domain, - urlpath: config.urlPath, - debug: config.debug, - version: config.version, - DROPBOX_APP_KEY: config.dropbox.appKey, - allowedUploadMimeTypes: config.allowedUploadMimeTypes -} - -ejs.renderFile(constpath, data, {}, function (err, str) { - if (err) throw new Error(err) - fs.writeFileSync(path.join(__dirname, './public/build/constant.js'), str) -}) - // server setup var app = express() var server = null @@ -129,7 +113,7 @@ if (config.csp.enable) { } i18n.configure({ - locales: ['en', 'zh', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko'], + locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko'], cookie: 'locale', directory: path.join(__dirname, '/locales'), updateFiles: config.updateI18nFiles @@ -179,6 +163,7 @@ app.use(passport.session()) app.use(require('./lib/web/middleware/checkURIValid')) // redirect url without trailing slashes app.use(require('./lib/web/middleware/redirectWithoutTrailingSlashes')) +app.use(require('./lib/web/middleware/codiMDVersion')) // routes need sessions // template files @@ -220,11 +205,21 @@ io.sockets.on('connection', realtime.connection) // listen function startListen () { - server.listen(config.port, function () { + var address + var listenCallback = function () { var schema = config.useSSL ? 'HTTPS' : 'HTTP' - logger.info('%s Server listening at port %d', schema, config.port) + logger.info('%s Server listening at %s', schema, address) realtime.maintenance = false - }) + } + + // use unix domain socket if 'path' is specified + if (config.path) { + address = config.path + server.listen(config.path, listenCallback) + } else { + address = config.host + ':' + config.port + server.listen(config.port, config.host, listenCallback) + } } // sync db then start listen @@ -250,7 +245,7 @@ process.on('uncaughtException', function (err) { // install exit handler function handleTermSignals () { - logger.info('hackmd has been killed by signal, try to exit gracefully...') + logger.info('CodiMD has been killed by signal, try to exit gracefully...') realtime.maintenance = true // disconnect all socket.io clients Object.keys(io.sockets.sockets).forEach(function (key) { diff --git a/app.json b/app.json index 36877e6..6a6142e 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,5 @@ { - "name": "HackMD", + "name": "CodiMD", "description": "Realtime collaborative markdown notes on all platforms", "keywords": [ "Collaborative", @@ -7,8 +7,8 @@ "Notes" ], "website": "https://hackmd.io", - "repository": "https://github.com/hackmdio/hackmd", - "logo": "https://github.com/hackmdio/hackmd/raw/master/public/hackmd-icon-1024.png", + "repository": "https://github.com/hackmdio/codimd", + "logo": "https://github.com/hackmdio/codimd/raw/master/public/codimd-icon-1024.png", "success_url": "/", "env": { "BUILD_ASSETS": { diff --git a/bin/setup b/bin/setup index 3edffc4..122cb7a 100755 --- a/bin/setup +++ b/bin/setup @@ -33,11 +33,10 @@ BUILD_ASSETS=false npm install cat << EOF -Edit the following config file to setup hackmd server and client. -Read more info at https://github.com/hackmdio/hackmd#configuration-files +Edit the following config file to setup CodiMD server and client. +Read more info at https://github.com/hackmdio/codimd#configuration-files -* config.json -- server config -* public/js/config.js -- client config +* config.json -- CodiMD config * .sequelizerc -- db config EOF diff --git a/config.json.example b/config.json.example index e07052b..1f2ec3d 100644 --- a/config.json.example +++ b/config.json.example @@ -11,7 +11,7 @@ }, "db": { "dialect": "sqlite", - "storage": "./db.hackmd.sqlite" + "storage": "./db.codimd.sqlite" } }, "production": { @@ -34,7 +34,7 @@ "db": { "username": "", "password": "", - "database": "hackmd", + "database": "codimd", "host": "localhost", "port": "5432", "dialect": "postgres" diff --git a/docs/guides/auth/github.md b/docs/guides/auth/github.md index 62910cb..d6a1095 100644 --- a/docs/guides/auth/github.md +++ b/docs/guides/auth/github.md @@ -1,6 +1,8 @@ Authentication guide - GitHub === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + 1. Sign-in or sign-up for a GitHub account 2. Navigate to developer settings in your GitHub account [here](https://github.com/settings/developers) and select the "OAuth Apps" tab 3. Click on the **New OAuth App** button, to create a new OAuth App: diff --git a/docs/guides/auth/gitlab-self-hosted.md b/docs/guides/auth/gitlab-self-hosted.md index 361ee95..89bc996 100644 --- a/docs/guides/auth/gitlab-self-hosted.md +++ b/docs/guides/auth/gitlab-self-hosted.md @@ -1,6 +1,8 @@ # GitLab (self-hosted) === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + 1. Sign in to your GitLab 2. Navigate to the application management page at `https://your.gitlab.domain/admin/applications` (admin permissions required) 3. Click **New application** to create a new application and fill out the registration form: diff --git a/docs/guides/auth/saml-onelogin.md b/docs/guides/auth/saml-onelogin.md index 245876c..02a5ffa 100644 --- a/docs/guides/auth/saml-onelogin.md +++ b/docs/guides/auth/saml-onelogin.md @@ -1,6 +1,8 @@ Authentication guide - SAML (OneLogin) === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + 1. Sign-in or sign-up for an OneLogin account. (available free trial for 2 weeks) 2. Go to the administration page. 3. Select the **APPS** menu and click on the **Add Apps**. diff --git a/docs/guides/auth/saml.md b/docs/guides/auth/saml.md index 9a51667..97ac9d3 100644 --- a/docs/guides/auth/saml.md +++ b/docs/guides/auth/saml.md @@ -1,6 +1,8 @@ Authentication guide - SAML === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + The basic procedure is the same as the case of OneLogin which is mentioned in [OneLogin-Guide](./saml-onelogin.md). If you want to match your IdP, you can use more configurations as below. * If your IdP accepts metadata XML of the service provider to ease configuraion, use this url to download metadata XML. diff --git a/docs/guides/auth/twitter.md b/docs/guides/auth/twitter.md index 02309ca..1b96288 100644 --- a/docs/guides/auth/twitter.md +++ b/docs/guides/auth/twitter.md @@ -1,6 +1,8 @@ Authentication guide - Twitter === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + 1. Sign-in or sign-up for a Twitter account 2. Go to the Twitter Application management page [here](https://apps.twitter.com/) 3. Click on the **Create New App** button to create a new Twitter app: diff --git a/docs/guides/minio-image-upload.md b/docs/guides/minio-image-upload.md index c2ba25b..7f5796c 100644 --- a/docs/guides/minio-image-upload.md +++ b/docs/guides/minio-image-upload.md @@ -1,6 +1,8 @@ -Minio Guide for HackMD +Minio Guide for CodiMD === +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* + 1. First of all you need to setup Minio itself. Please refer to the [official Minio docs](https://docs.minio.io/) for an diff --git a/docs/guides/providing-terms.md b/docs/guides/providing-terms.md new file mode 100644 index 0000000..1e580df --- /dev/null +++ b/docs/guides/providing-terms.md @@ -0,0 +1,17 @@ +Setup your terms of use +=== + +To setup your terms of use, you need to provide a document called `terms-of-use.md` which contains them. Of course written in Markdown. + +It has to be provided under `./public/docs/` and will be automatically turned into a CodiMD document. It will also automatically updated as soon as you change the document on disk. + +As soon as the file exists a link will show up in the bottom part along with the release notes and link to them. + +Setup your privacy policy +=== + +To add a privacy policy you can use the same technique as for the terms of use. The main difference is that the document is called `privacy.md`. + +See our example file `./public/docs/privacy.md.example` container some useful hints for writing your own privacy policy. + +As with the terms of use, a link to the privacy notices will show up in the area where the release notes are provided on the index page. diff --git a/docs/guides/s3-image-upload.md b/docs/guides/s3-image-upload.md index ff9598a..dc5e420 100644 --- a/docs/guides/s3-image-upload.md +++ b/docs/guides/s3-image-upload.md @@ -1,4 +1,6 @@ -# Guide - Setup HackMD S3 image upload +# Guide - Setup CodiMD S3 image upload + +***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!* 1. Go to [AWS S3 console](https://console.aws.amazon.com/s3/home) and create a new bucket. diff --git a/lib/config/default.js b/lib/config/default.js index 1b124b3..6096bce 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -3,6 +3,7 @@ module.exports = { domain: '', urlPath: '', + host: '0.0.0.0', port: 3000, urlAddPort: false, allowOrigin: ['localhost'], @@ -41,10 +42,12 @@ module.exports = { defaultNotePath: './public/default.md', docsPath: './public/docs', indexPath: './public/views/index.ejs', - hackmdPath: './public/views/hackmd.ejs', + codimdPath: './public/views/codimd.ejs', errorPath: './public/views/error.ejs', prettyPath: './public/views/pretty.ejs', slidePath: './public/views/slide.ejs', + constantsPath: './public/js/lib/common/constant.ejs', + uploadsPath: './public/uploads', // session sessionName: 'connect.sid', sessionSecret: 'secret', @@ -145,5 +148,6 @@ module.exports = { }, email: true, allowEmailRegister: true, + allowGravatar: true, allowPDFExport: true } diff --git a/lib/config/environment.js b/lib/config/environment.js index e1c1156..6c4ce92 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -3,122 +3,125 @@ const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils') module.exports = { - domain: process.env.HMD_DOMAIN, - urlPath: process.env.HMD_URL_PATH, - port: toIntegerConfig(process.env.HMD_PORT), - urlAddPort: toBooleanConfig(process.env.HMD_URL_ADDPORT), - useSSL: toBooleanConfig(process.env.HMD_USESSL), + domain: process.env.CMD_DOMAIN, + urlPath: process.env.CMD_URL_PATH, + host: process.env.CMD_HOST, + port: toIntegerConfig(process.env.CMD_PORT), + path: process.env.CMD_PATH, + urlAddPort: toBooleanConfig(process.env.CMD_URL_ADDPORT), + useSSL: toBooleanConfig(process.env.CMD_USESSL), hsts: { - enable: toBooleanConfig(process.env.HMD_HSTS_ENABLE), - maxAgeSeconds: process.env.HMD_HSTS_MAX_AGE, - includeSubdomains: toBooleanConfig(process.env.HMD_HSTS_INCLUDE_SUBDOMAINS), - preload: toBooleanConfig(process.env.HMD_HSTS_PRELOAD) + enable: toBooleanConfig(process.env.CMD_HSTS_ENABLE), + maxAgeSeconds: process.env.CMD_HSTS_MAX_AGE, + includeSubdomains: toBooleanConfig(process.env.CMD_HSTS_INCLUDE_SUBDOMAINS), + preload: toBooleanConfig(process.env.CMD_HSTS_PRELOAD) }, csp: { - enable: toBooleanConfig(process.env.HMD_CSP_ENABLE), - reportURI: process.env.HMD_CSP_REPORTURI + enable: toBooleanConfig(process.env.CMD_CSP_ENABLE), + reportURI: process.env.CMD_CSP_REPORTURI }, - protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL), - allowOrigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN), - useCDN: toBooleanConfig(process.env.HMD_USECDN), - allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS), - allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS), - allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), - defaultPermission: process.env.HMD_DEFAULT_PERMISSION, - dbURL: process.env.HMD_DB_URL, - sessionSecret: process.env.HMD_SESSION_SECRET, - sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE), - imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE, + protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), + allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN), + useCDN: toBooleanConfig(process.env.CMD_USECDN), + allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS), + allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS), + allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL), + defaultPermission: process.env.CMD_DEFAULT_PERMISSION, + dbURL: process.env.CMD_DB_URL, + sessionSecret: process.env.CMD_SESSION_SECRET, + sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE), + imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE, imgur: { - clientID: process.env.HMD_IMGUR_CLIENTID + clientID: process.env.CMD_IMGUR_CLIENTID }, s3: { - accessKeyId: process.env.HMD_S3_ACCESS_KEY_ID, - secretAccessKey: process.env.HMD_S3_SECRET_ACCESS_KEY, - region: process.env.HMD_S3_REGION + accessKeyId: process.env.CMD_S3_ACCESS_KEY_ID, + secretAccessKey: process.env.CMD_S3_SECRET_ACCESS_KEY, + region: process.env.CMD_S3_REGION }, minio: { - accessKey: process.env.HMD_MINIO_ACCESS_KEY, - secretKey: process.env.HMD_MINIO_SECRET_KEY, - endPoint: process.env.HMD_MINIO_ENDPOINT, - secure: toBooleanConfig(process.env.HMD_MINIO_SECURE), - port: toIntegerConfig(process.env.HMD_MINIO_PORT) + accessKey: process.env.CMD_MINIO_ACCESS_KEY, + secretKey: process.env.CMD_MINIO_SECRET_KEY, + endPoint: process.env.CMD_MINIO_ENDPOINT, + secure: toBooleanConfig(process.env.CMD_MINIO_SECURE), + port: toIntegerConfig(process.env.CMD_MINIO_PORT) }, - s3bucket: process.env.HMD_S3_BUCKET, + s3bucket: process.env.CMD_S3_BUCKET, azure: { - connectionString: process.env.HMD_AZURE_CONNECTION_STRING, - container: process.env.HMD_AZURE_CONTAINER + connectionString: process.env.CMD_AZURE_CONNECTION_STRING, + container: process.env.CMD_AZURE_CONTAINER }, facebook: { - clientID: process.env.HMD_FACEBOOK_CLIENTID, - clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET + clientID: process.env.CMD_FACEBOOK_CLIENTID, + clientSecret: process.env.CMD_FACEBOOK_CLIENTSECRET }, twitter: { - consumerKey: process.env.HMD_TWITTER_CONSUMERKEY, - consumerSecret: process.env.HMD_TWITTER_CONSUMERSECRET + consumerKey: process.env.CMD_TWITTER_CONSUMERKEY, + consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET }, github: { - clientID: process.env.HMD_GITHUB_CLIENTID, - clientSecret: process.env.HMD_GITHUB_CLIENTSECRET + clientID: process.env.CMD_GITHUB_CLIENTID, + clientSecret: process.env.CMD_GITHUB_CLIENTSECRET }, gitlab: { - baseURL: process.env.HMD_GITLAB_BASEURL, - clientID: process.env.HMD_GITLAB_CLIENTID, - clientSecret: process.env.HMD_GITLAB_CLIENTSECRET, - scope: process.env.HMD_GITLAB_SCOPE + baseURL: process.env.CMD_GITLAB_BASEURL, + clientID: process.env.CMD_GITLAB_CLIENTID, + clientSecret: process.env.CMD_GITLAB_CLIENTSECRET, + scope: process.env.CMD_GITLAB_SCOPE }, mattermost: { - baseURL: process.env.HMD_MATTERMOST_BASEURL, - clientID: process.env.HMD_MATTERMOST_CLIENTID, - clientSecret: process.env.HMD_MATTERMOST_CLIENTSECRET + baseURL: process.env.CMD_MATTERMOST_BASEURL, + clientID: process.env.CMD_MATTERMOST_CLIENTID, + clientSecret: process.env.CMD_MATTERMOST_CLIENTSECRET }, oauth2: { - baseURL: process.env.HMD_OAUTH2_BASEURL, - userProfileURL: process.env.HMD_OAUTH2_USER_PROFILE_URL, - userProfileUsernameAttr: process.env.HMD_OAUTH2_USER_PROFILE_USERNAME_ATTR, - userProfileDisplayNameAttr: process.env.HMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR, - userProfileEmailAttr: process.env.HMD_OAUTH2_USER_PROFILE_EMAIL_ATTR, - tokenURL: process.env.HMD_OAUTH2_TOKEN_URL, - authorizationURL: process.env.HMD_OAUTH2_AUTHORIZATION_URL, - clientID: process.env.HMD_OAUTH2_CLIENT_ID, - clientSecret: process.env.HMD_OAUTH2_CLIENT_SECRET + baseURL: process.env.CMD_OAUTH2_BASEURL, + userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL, + userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR, + userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR, + userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR, + tokenURL: process.env.CMD_OAUTH2_TOKEN_URL, + authorizationURL: process.env.CMD_OAUTH2_AUTHORIZATION_URL, + clientID: process.env.CMD_OAUTH2_CLIENT_ID, + clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET }, dropbox: { - clientID: process.env.HMD_DROPBOX_CLIENTID, - clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET, - appKey: process.env.HMD_DROPBOX_APPKEY + clientID: process.env.CMD_DROPBOX_CLIENTID, + clientSecret: process.env.CMD_DROPBOX_CLIENTSECRET, + appKey: process.env.CMD_DROPBOX_APPKEY }, google: { - clientID: process.env.HMD_GOOGLE_CLIENTID, - clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET + clientID: process.env.CMD_GOOGLE_CLIENTID, + clientSecret: process.env.CMD_GOOGLE_CLIENTSECRET }, ldap: { - providerName: process.env.HMD_LDAP_PROVIDERNAME, - url: process.env.HMD_LDAP_URL, - bindDn: process.env.HMD_LDAP_BINDDN, - bindCredentials: process.env.HMD_LDAP_BINDCREDENTIALS, - searchBase: process.env.HMD_LDAP_SEARCHBASE, - searchFilter: process.env.HMD_LDAP_SEARCHFILTER, - searchAttributes: toArrayConfig(process.env.HMD_LDAP_SEARCHATTRIBUTES), - usernameField: process.env.HMD_LDAP_USERNAMEFIELD, - useridField: process.env.HMD_LDAP_USERIDFIELD, - tlsca: process.env.HMD_LDAP_TLS_CA + providerName: process.env.CMD_LDAP_PROVIDERNAME, + url: process.env.CMD_LDAP_URL, + bindDn: process.env.CMD_LDAP_BINDDN, + bindCredentials: process.env.CMD_LDAP_BINDCREDENTIALS, + searchBase: process.env.CMD_LDAP_SEARCHBASE, + searchFilter: process.env.CMD_LDAP_SEARCHFILTER, + searchAttributes: toArrayConfig(process.env.CMD_LDAP_SEARCHATTRIBUTES), + usernameField: process.env.CMD_LDAP_USERNAMEFIELD, + useridField: process.env.CMD_LDAP_USERIDFIELD, + tlsca: process.env.CMD_LDAP_TLS_CA }, saml: { - idpSsoUrl: process.env.HMD_SAML_IDPSSOURL, - idpCert: process.env.HMD_SAML_IDPCERT, - issuer: process.env.HMD_SAML_ISSUER, - identifierFormat: process.env.HMD_SAML_IDENTIFIERFORMAT, - groupAttribute: process.env.HMD_SAML_GROUPATTRIBUTE, - externalGroups: toArrayConfig(process.env.HMD_SAML_EXTERNALGROUPS, '|', []), - requiredGroups: toArrayConfig(process.env.HMD_SAML_REQUIREDGROUPS, '|', []), + idpSsoUrl: process.env.CMD_SAML_IDPSSOURL, + idpCert: process.env.CMD_SAML_IDPCERT, + issuer: process.env.CMD_SAML_ISSUER, + identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT, + groupAttribute: process.env.CMD_SAML_GROUPATTRIBUTE, + externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []), + requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []), attribute: { - id: process.env.HMD_SAML_ATTRIBUTE_ID, - username: process.env.HMD_SAML_ATTRIBUTE_USERNAME, - email: process.env.HMD_SAML_ATTRIBUTE_EMAIL + id: process.env.CMD_SAML_ATTRIBUTE_ID, + username: process.env.CMD_SAML_ATTRIBUTE_USERNAME, + email: process.env.CMD_SAML_ATTRIBUTE_EMAIL } }, - email: toBooleanConfig(process.env.HMD_EMAIL), - allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER), - allowPDFExport: toBooleanConfig(process.env.HMD_ALLOW_PDF_EXPORT) + email: toBooleanConfig(process.env.CMD_EMAIL), + allowEmailRegister: toBooleanConfig(process.env.CMD_ALLOW_EMAIL_REGISTER), + allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR), + allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT) } diff --git a/lib/config/hackmdEnvironment.js b/lib/config/hackmdEnvironment.js new file mode 100644 index 0000000..e1c1156 --- /dev/null +++ b/lib/config/hackmdEnvironment.js @@ -0,0 +1,124 @@ +'use strict' + +const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils') + +module.exports = { + domain: process.env.HMD_DOMAIN, + urlPath: process.env.HMD_URL_PATH, + port: toIntegerConfig(process.env.HMD_PORT), + urlAddPort: toBooleanConfig(process.env.HMD_URL_ADDPORT), + useSSL: toBooleanConfig(process.env.HMD_USESSL), + hsts: { + enable: toBooleanConfig(process.env.HMD_HSTS_ENABLE), + maxAgeSeconds: process.env.HMD_HSTS_MAX_AGE, + includeSubdomains: toBooleanConfig(process.env.HMD_HSTS_INCLUDE_SUBDOMAINS), + preload: toBooleanConfig(process.env.HMD_HSTS_PRELOAD) + }, + csp: { + enable: toBooleanConfig(process.env.HMD_CSP_ENABLE), + reportURI: process.env.HMD_CSP_REPORTURI + }, + protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL), + allowOrigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN), + useCDN: toBooleanConfig(process.env.HMD_USECDN), + allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS), + allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS), + allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), + defaultPermission: process.env.HMD_DEFAULT_PERMISSION, + dbURL: process.env.HMD_DB_URL, + sessionSecret: process.env.HMD_SESSION_SECRET, + sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE), + imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE, + imgur: { + clientID: process.env.HMD_IMGUR_CLIENTID + }, + s3: { + accessKeyId: process.env.HMD_S3_ACCESS_KEY_ID, + secretAccessKey: process.env.HMD_S3_SECRET_ACCESS_KEY, + region: process.env.HMD_S3_REGION + }, + minio: { + accessKey: process.env.HMD_MINIO_ACCESS_KEY, + secretKey: process.env.HMD_MINIO_SECRET_KEY, + endPoint: process.env.HMD_MINIO_ENDPOINT, + secure: toBooleanConfig(process.env.HMD_MINIO_SECURE), + port: toIntegerConfig(process.env.HMD_MINIO_PORT) + }, + s3bucket: process.env.HMD_S3_BUCKET, + azure: { + connectionString: process.env.HMD_AZURE_CONNECTION_STRING, + container: process.env.HMD_AZURE_CONTAINER + }, + facebook: { + clientID: process.env.HMD_FACEBOOK_CLIENTID, + clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET + }, + twitter: { + consumerKey: process.env.HMD_TWITTER_CONSUMERKEY, + consumerSecret: process.env.HMD_TWITTER_CONSUMERSECRET + }, + github: { + clientID: process.env.HMD_GITHUB_CLIENTID, + clientSecret: process.env.HMD_GITHUB_CLIENTSECRET + }, + gitlab: { + baseURL: process.env.HMD_GITLAB_BASEURL, + clientID: process.env.HMD_GITLAB_CLIENTID, + clientSecret: process.env.HMD_GITLAB_CLIENTSECRET, + scope: process.env.HMD_GITLAB_SCOPE + }, + mattermost: { + baseURL: process.env.HMD_MATTERMOST_BASEURL, + clientID: process.env.HMD_MATTERMOST_CLIENTID, + clientSecret: process.env.HMD_MATTERMOST_CLIENTSECRET + }, + oauth2: { + baseURL: process.env.HMD_OAUTH2_BASEURL, + userProfileURL: process.env.HMD_OAUTH2_USER_PROFILE_URL, + userProfileUsernameAttr: process.env.HMD_OAUTH2_USER_PROFILE_USERNAME_ATTR, + userProfileDisplayNameAttr: process.env.HMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR, + userProfileEmailAttr: process.env.HMD_OAUTH2_USER_PROFILE_EMAIL_ATTR, + tokenURL: process.env.HMD_OAUTH2_TOKEN_URL, + authorizationURL: process.env.HMD_OAUTH2_AUTHORIZATION_URL, + clientID: process.env.HMD_OAUTH2_CLIENT_ID, + clientSecret: process.env.HMD_OAUTH2_CLIENT_SECRET + }, + dropbox: { + clientID: process.env.HMD_DROPBOX_CLIENTID, + clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET, + appKey: process.env.HMD_DROPBOX_APPKEY + }, + google: { + clientID: process.env.HMD_GOOGLE_CLIENTID, + clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET + }, + ldap: { + providerName: process.env.HMD_LDAP_PROVIDERNAME, + url: process.env.HMD_LDAP_URL, + bindDn: process.env.HMD_LDAP_BINDDN, + bindCredentials: process.env.HMD_LDAP_BINDCREDENTIALS, + searchBase: process.env.HMD_LDAP_SEARCHBASE, + searchFilter: process.env.HMD_LDAP_SEARCHFILTER, + searchAttributes: toArrayConfig(process.env.HMD_LDAP_SEARCHATTRIBUTES), + usernameField: process.env.HMD_LDAP_USERNAMEFIELD, + useridField: process.env.HMD_LDAP_USERIDFIELD, + tlsca: process.env.HMD_LDAP_TLS_CA + }, + saml: { + idpSsoUrl: process.env.HMD_SAML_IDPSSOURL, + idpCert: process.env.HMD_SAML_IDPCERT, + issuer: process.env.HMD_SAML_ISSUER, + identifierFormat: process.env.HMD_SAML_IDENTIFIERFORMAT, + groupAttribute: process.env.HMD_SAML_GROUPATTRIBUTE, + externalGroups: toArrayConfig(process.env.HMD_SAML_EXTERNALGROUPS, '|', []), + requiredGroups: toArrayConfig(process.env.HMD_SAML_REQUIREDGROUPS, '|', []), + attribute: { + id: process.env.HMD_SAML_ATTRIBUTE_ID, + username: process.env.HMD_SAML_ATTRIBUTE_USERNAME, + email: process.env.HMD_SAML_ATTRIBUTE_EMAIL + } + }, + email: toBooleanConfig(process.env.HMD_EMAIL), + allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER), + allowPDFExport: toBooleanConfig(process.env.HMD_ALLOW_PDF_EXPORT) +} diff --git a/lib/config/index.js b/lib/config/index.js index 7853dba..ac03fcd 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -33,6 +33,7 @@ merge(config, debugConfig) merge(config, packageConfig) merge(config, fileConfig) merge(config, require('./oldEnvironment')) +merge(config, require('./hackmdEnvironment')) merge(config, require('./environment')) merge(config, require('./dockerSecret')) @@ -122,6 +123,11 @@ for (let i = keys.length; i--;) { } } +// Notify users about the prefix change and inform them they use legacy prefix for environment variables +if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) { + logger.warn('Using legacy HMD prefix for environment variables. Please change your variables in future. For details see: https://github.com/hackmdio/codimd#environment-variables-will-overwrite-other-server-configs') +} + // Generate session secret if it stays on default values if (config.sessionSecret === 'secret') { logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.') @@ -133,7 +139,7 @@ if (config.sessionSecret === 'secret') { // Validate upload upload providers if (['filesystem', 's3', 'minio', 'imgur', 'azure'].indexOf(config.imageUploadType) === -1) { logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure" or "imgur". Defaulting to "imgur"') - config.imageUploadType = 'imgur' + config.imageUploadType = 'filesystem' } // figure out mime types for image uploads @@ -169,10 +175,12 @@ config.tmpPath = path.join(appRootPath, config.tmpPath) config.defaultNotePath = path.join(appRootPath, config.defaultNotePath) config.docsPath = path.join(appRootPath, config.docsPath) config.indexPath = path.join(appRootPath, config.indexPath) -config.hackmdPath = path.join(appRootPath, config.hackmdPath) +config.codimdPath = path.join(appRootPath, config.codimdPath) config.errorPath = path.join(appRootPath, config.errorPath) config.prettyPath = path.join(appRootPath, config.prettyPath) config.slidePath = path.join(appRootPath, config.slidePath) +config.constantsPath = path.join(appRootPath, config.constantsPath) +config.uploadsPath = path.join(appRootPath, config.uploadsPath) // make config readonly config = deepFreeze(config) diff --git a/lib/csp.js b/lib/csp.js index d0f906a..0987df8 100644 --- a/lib/csp.js +++ b/lib/csp.js @@ -6,7 +6,7 @@ var CspStrategy = {} var defaultDirectives = { defaultSrc: ['\'self\''], scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''], - // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/hackmd/issues/594 + // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/codimd/issues/594 imgSrc: ['*'], styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views fontSrc: ['\'self\'', 'https://public.slidesharecdn.com'], @@ -71,7 +71,7 @@ function addInlineScriptExceptions (directives) { directives.scriptSrc.push(getCspNonce) // TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html // Any more clean solution appreciated. - directives.scriptSrc.push('\'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=\'') + directives.scriptSrc.push('\'sha256-L0TsyAQLAc0koby5DCbFAwFfRs9ZxesA+4xg0QDSrdI=\'') } function getCspNonce (req, res) { diff --git a/lib/history.js b/lib/history.js index c7d2472..9c389bf 100644 --- a/lib/history.js +++ b/lib/history.js @@ -31,6 +31,15 @@ function getHistory (userid, callback) { history = JSON.parse(user.history) // migrate LZString encoded note id to base64url encoded note id for (let i = 0, l = history.length; i < l; i++) { + // Calculate minimal string length for an UUID that is encoded + // base64 encoded and optimize comparsion by using -1 + // this should make a lot of LZ-String parsing errors obsolete + // as we can assume that a nodeId that is 48 chars or longer is a + // noteID. + const base64UuidLength = ((4 * 36) / 3) - 1 + if (!(history[i].id.length > base64UuidLength)) { + continue + } try { let id = LZString.decompressFromBase64(history[i].id) if (id && models.Note.checkNoteIdValid(id)) { @@ -38,7 +47,11 @@ function getHistory (userid, callback) { } } catch (err) { // most error here comes from LZString, ignore - logger.error(err) + if (err.message === 'Cannot read property \'charAt\' of undefined') { + logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.') + } else { + logger.error(err) + } } } history = parseHistoryToObject(history) diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index b5b1d9e..53fa011 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -1,5 +1,6 @@ 'use strict' // external modules +const md5 = require('blueimp-md5') const randomcolor = require('randomcolor') const config = require('./config') @@ -24,6 +25,17 @@ exports.generateAvatar = function (name) { return svg } -exports.generateAvatarURL = function (name) { - return config.serverURL + '/user/' + name + '/avatar.svg' +exports.generateAvatarURL = function (name, email = '', big = true) { + let photo + if (email !== '' && config.allowGravatar) { + photo = 'https://www.gravatar.com/avatar/' + md5(email.toLowerCase()) + if (big) { + photo += '?s=400' + } else { + photo += '?s=96' + } + } else { + photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || md5(email.toLowerCase())) + '/avatar.svg' + } + return photo } diff --git a/lib/migrations/20180525153000-user-add-delete-token.js b/lib/migrations/20180525153000-user-add-delete-token.js new file mode 100644 index 0000000..642fa5d --- /dev/null +++ b/lib/migrations/20180525153000-user-add-delete-token.js @@ -0,0 +1,13 @@ +'use strict' +module.exports = { + up: function (queryInterface, Sequelize) { + return queryInterface.addColumn('Users', 'deleteToken', { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4 + }) + }, + + down: function (queryInterface, Sequelize) { + return queryInterface.removeColumn('Users', 'deleteToken') + } +} diff --git a/lib/models/author.js b/lib/models/author.js index 8b4f74e..03f832a 100644 --- a/lib/models/author.js +++ b/lib/models/author.js @@ -24,12 +24,16 @@ module.exports = function (sequelize, DataTypes) { Author.belongsTo(models.Note, { foreignKey: 'noteId', as: 'note', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) Author.belongsTo(models.User, { foreignKey: 'userId', as: 'user', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) } } diff --git a/lib/models/note.js b/lib/models/note.js index c8e69e4..e7722fa 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -85,13 +85,15 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DATE } }, { - paranoid: true, + paranoid: false, classMethods: { associate: function (models) { Note.belongsTo(models.User, { foreignKey: 'ownerId', as: 'owner', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) Note.belongsTo(models.User, { foreignKey: 'lastchangeuserId', @@ -225,7 +227,11 @@ module.exports = function (sequelize, DataTypes) { var id = LZString.decompressFromBase64(noteId) if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } } catch (err) { - logger.error(err) + if (err.message === 'Cannot read property \'charAt\' of undefined') { + logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.') + } else { + logger.error(err) + } return _callback(null, null) } }, diff --git a/lib/models/revision.js b/lib/models/revision.js index 9ecd14d..8bc95cb 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -102,7 +102,9 @@ module.exports = function (sequelize, DataTypes) { Revision.belongsTo(models.Note, { foreignKey: 'noteId', as: 'note', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) }, getNoteRevisions: function (note, callback) { diff --git a/lib/models/user.js b/lib/models/user.js index 0f6acd6..1bd8c74 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,6 +1,5 @@ 'use strict' // external modules -var md5 = require('blueimp-md5') var Sequelize = require('sequelize') var scrypt = require('scrypt') @@ -31,6 +30,10 @@ module.exports = function (sequelize, DataTypes) { refreshToken: { type: DataTypes.TEXT }, + deleteToken: { + type: DataTypes.UUID, + defaultValue: Sequelize.UUIDV4 + }, email: { type: Sequelize.TEXT, validate: { @@ -66,6 +69,9 @@ module.exports = function (sequelize, DataTypes) { }) }, getProfile: function (user) { + if (!user) { + return null + } return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) }, parseProfile: function (profile) { @@ -121,10 +127,7 @@ module.exports = function (sequelize, DataTypes) { } break case 'dropbox': - // no image api provided, use gravatar - photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value) - if (bigger) photo += '?s=400' - else photo += '?s=96' + photo = generateAvatarURL('', profile.emails[0].value, bigger) break case 'google': photo = profile.photos[0].value @@ -132,35 +135,19 @@ module.exports = function (sequelize, DataTypes) { else photo = photo.replace(/(\?sz=)\d*$/i, '$196') break case 'ldap': - // no image api provided, - // use gravatar if email exists, - // otherwise generate a letter avatar - if (profile.emails[0]) { - photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) - if (bigger) photo += '?s=400' - else photo += '?s=96' - } else { - photo = generateAvatarURL(profile.username) - } + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) break case 'saml': - if (profile.emails[0]) { - photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) - if (bigger) photo += '?s=400' - else photo += '?s=96' - } else { - photo = generateAvatarURL(profile.username) - } + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) break } return photo }, parseProfileByEmail: function (email) { - var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email) return { name: email.substring(0, email.lastIndexOf('@')), - photo: photoUrl + '?s=96', - biggerphoto: photoUrl + '?s=400' + photo: generateAvatarURL('', email, false), + biggerphoto: generateAvatarURL('', email, true) } } } diff --git a/lib/realtime.js b/lib/realtime.js index 070bde2..f6c62d4 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -486,11 +486,13 @@ function startConnection (socket) { for (var i = 0; i < note.authors.length; i++) { var author = note.authors[i] var profile = models.User.getProfile(author.user) - authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: profile.photo, - name: profile.name + if (profile) { + authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: profile.photo, + name: profile.name + } } } diff --git a/lib/response.js b/lib/response.js index 89ade7a..3a31c51 100644 --- a/lib/response.js +++ b/lib/response.js @@ -2,6 +2,7 @@ // response // external modules var fs = require('fs') +var path = require('path') var markdownpdf = require('markdown-pdf') var shortId = require('shortid') var querystring = require('querystring') @@ -61,7 +62,10 @@ function responseError (res, code, detail, msg) { } function showIndex (req, res, next) { - res.render(config.indexPath, { + var authStatus = req.isAuthenticated() + var deleteToken = '' + + var data = { url: config.serverURL, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, @@ -81,13 +85,31 @@ function showIndex (req, res, next) { email: config.isEmailEnable, allowEmailRegister: config.allowEmailRegister, allowPDFExport: config.allowPDFExport, - signin: req.isAuthenticated(), + signin: authStatus, infoMessage: req.flash('info'), - errorMessage: req.flash('error') - }) + errorMessage: req.flash('error'), + privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), + termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')), + deleteToken: deleteToken + } + + if (authStatus) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (user) { + data.deleteToken = user.deleteToken + res.render(config.indexPath, data) + } + }) + } else { + res.render(config.indexPath, data) + } } -function responseHackMD (res, note) { +function responseCodiMD (res, note) { var body = note.content var extracted = models.Note.extractMeta(body) var meta = models.Note.parseMeta(extracted.meta) @@ -97,7 +119,7 @@ function responseHackMD (res, note) { 'Cache-Control': 'private', // only cache by client 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling }) - res.render(config.hackmdPath, { + res.render(config.codimdPath, { url: config.serverURL, title: title, useCDN: config.useCDN, @@ -123,6 +145,8 @@ function responseHackMD (res, note) { function newNote (req, res, next) { var owner = null + var body = req.body ? req.body : '' + body = body.replace(/[\r]/g, '') if (req.isAuthenticated()) { owner = req.user.id } else if (!config.allowAnonymous) { @@ -131,7 +155,7 @@ function newNote (req, res, next) { models.Note.create({ ownerId: owner, alias: req.alias ? req.alias : null, - content: req.body ? req.body : '' + content: body }).then(function (note) { return res.redirect(config.serverURL + '/' + models.Note.encodeNoteId(note.id)) }).catch(function (err) { @@ -190,7 +214,7 @@ function showNote (req, res, next) { var noteId = req.params.noteId var id = models.Note.encodeNoteId(note.id) if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) { return res.redirect(config.serverURL + '/' + (note.alias || id)) } - return responseHackMD(res, note) + return responseCodiMD(res, note) }) } @@ -305,15 +329,18 @@ function actionInfo (req, res, note) { } function actionPDF (req, res, note) { + var url = config.serverURL || 'http://' + req.get('host') var body = note.content var extracted = models.Note.extractMeta(body) + var content = extracted.markdown var title = models.Note.decodeTitle(note.title) if (!fs.existsSync(config.tmpPath)) { fs.mkdirSync(config.tmpPath) } var path = config.tmpPath + '/' + Date.now() + '.pdf' - markdownpdf().from.string(extracted.markdown).to(path, function () { + content = content.replace(/\]\(\//g, '](' + url + '/') + markdownpdf().from.string(content).to(path, function () { var stream = fs.createReadStream(path) var filename = title // Be careful of special characters @@ -500,7 +527,7 @@ function githubActionGist (req, res, note) { request({ url: gistUrl, headers: { - 'User-Agent': 'HackMD', + 'User-Agent': 'CodiMD', 'Authorization': 'token ' + accessToken }, method: 'POST', diff --git a/lib/web/auth/ldap/index.js b/lib/web/auth/ldap/index.js index ab1cb4c..8d71c18 100644 --- a/lib/web/auth/ldap/index.js +++ b/lib/web/auth/ldap/index.js @@ -15,7 +15,7 @@ let ldapAuth = module.exports = Router() passport.use(new LDAPStrategy({ server: { url: config.ldap.url || null, - bindDn: config.ldap.bindDn || null, + bindDN: config.ldap.bindDn || null, bindCredentials: config.ldap.bindCredentials || null, searchBase: config.ldap.searchBase || null, searchFilter: config.ldap.searchFilter || null, diff --git a/lib/web/imageRouter/filesystem.js b/lib/web/imageRouter/filesystem.js index 145876a..4bf82b3 100644 --- a/lib/web/imageRouter/filesystem.js +++ b/lib/web/imageRouter/filesystem.js @@ -15,5 +15,5 @@ exports.uploadImage = function (imagePath, callback) { return } - callback(null, url.resolve(config.serverURL + '/', imagePath.match(/^public\/(.+$)/)[1])) + callback(null, url.resolve(config.serverURL + '/', imagePath.match(/public\/(.+)$/)[1])) } diff --git a/lib/web/imageRouter/index.js b/lib/web/imageRouter/index.js index f7e031a..f3c2dec 100644 --- a/lib/web/imageRouter/index.js +++ b/lib/web/imageRouter/index.js @@ -16,7 +16,7 @@ imageRouter.post('/uploadimage', function (req, res) { form.keepExtensions = true if (config.imageUploadType === 'filesystem') { - form.uploadDir = 'public/uploads' + form.uploadDir = config.uploadsPath } form.parse(req, function (err, fields, files) { diff --git a/lib/web/middleware/codiMDVersion.js b/lib/web/middleware/codiMDVersion.js new file mode 100644 index 0000000..e777587 --- /dev/null +++ b/lib/web/middleware/codiMDVersion.js @@ -0,0 +1,10 @@ +'use strict' + +const config = require('../../config') + +module.exports = function (req, res, next) { + res.set({ + 'CodiMD-Version': config.version + }) + return next() +} diff --git a/lib/web/statusRouter.js b/lib/web/statusRouter.js index d22fac4..7ecf383 100644 --- a/lib/web/statusRouter.js +++ b/lib/web/statusRouter.js @@ -18,7 +18,7 @@ statusRouter.get('/status', function (req, res, next) { res.set({ 'Cache-Control': 'private', // only cache by client 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling - 'HackMD-Version': config.version + 'Content-Type': 'application/json' }) res.send(data) }) @@ -90,3 +90,20 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) { } } }) + +statusRouter.get('/config', function (req, res) { + var data = { + domain: config.domain, + urlpath: config.urlPath, + debug: config.debug, + version: config.version, + DROPBOX_APP_KEY: config.dropbox.appKey, + allowedUploadMimeTypes: config.allowedUploadMimeTypes + } + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling + 'Content-Type': 'application/javascript' + }) + res.render(config.constantsPath, data) +}) diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index 963961c..db786d5 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -1,8 +1,11 @@ 'use strict' +const archiver = require('archiver') +const async = require('async') const Router = require('express').Router const response = require('../response') +const config = require('../config') const models = require('../models') const logger = require('../logger') const {generateAvatar} = require('../letter-avatars') @@ -36,6 +39,87 @@ UserRouter.get('/me', function (req, res) { } }) +// delete the currently authenticated user +UserRouter.get('/me/delete/:token?', function (req, res) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { + return response.errorNotFound(res) + } + if (user.deleteToken === req.params.token) { + user.destroy().then(function () { + res.redirect(config.serverURL + '/') + }) + } else { + return response.errorForbidden(res) + } + }).catch(function (err) { + logger.error('delete user failed: ' + err) + return response.errorInternalError(res) + }) + } else { + return response.errorForbidden(res) + } +}) + +// export the data of the authenticated user +UserRouter.get('/me/export', function (req, res) { + if (req.isAuthenticated()) { + // let output = fs.createWriteStream(__dirname + '/example.zip'); + let archive = archiver('zip', { + zlib: { level: 3 } // Sets the compression level. + }) + res.setHeader('Content-Type', 'application/zip') + res.attachment('archive.zip') + archive.pipe(res) + archive.on('error', function (err) { + logger.error('export user data failed: ' + err) + return response.errorInternalError(res) + }) + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + models.Note.findAll({ + where: { + ownerId: user.id + } + }).then(function (notes) { + let list = [] + async.each(notes, function (note, callback) { + let title + let extension = '' + do { + title = note.title + extension + extension++ + } while (list.indexOf(title) !== -1) + + list.push(title) + logger.debug('Write: ' + title + '.md') + archive.append(Buffer.from(note.content), { name: title + '.md', date: note.lastchangeAt }) + callback(null, null) + }, function (err) { + if (err) { + return response.errorInternalError(res) + } + + archive.finalize() + }) + }) + }).catch(function (err) { + logger.error('export user data failed: ' + err) + return response.errorInternalError(res) + }) + } else { + return response.errorForbidden(res) + } +}) + UserRouter.get('/user/:username/avatar.svg', function (req, res, next) { res.setHeader('Content-Type', 'image/svg+xml') res.setHeader('Cache-Control', 'public, max-age=86400') diff --git a/locales/de.json b/locales/de.json index b253910..40d5546 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,109 +1,115 @@ { - "Collaborative markdown notes": "Gemeinschaftliche Markdown Notizen", - "Realtime collaborative markdown notes on all platforms.": "Gemeinschaftliche Notizen in Echtzeit auf allen Plattformen.", - "Best way to write and share your knowledge in markdown.": "Der beste Weg, Notizen zu schreiben und teilen.", - "Intro": "Intro", - "History": "Verlauf", - "New guest note": "Neue Gast Notiz", - "Collaborate with URL": "Zusammenarbeiten mit URL", - "Support charts and MathJax": "Unterstützt charts und Mathjax", - "Support slide mode": "Unterstützt Präsentationsmodus", - "Sign In": "Einloggen", - "Below is the history from browser": "Lokaler Browserverlauf", - "Welcome!": "Willkommen!", - "New note": "Neue Notiz", - "or": "oder", - "Sign Out": "Ausloggen", - "Explore all features": "Alle Funktionen", - "Select tags...": "Tags auswählen ...", - "Search keyword...": "Suche nach Stichwort ...", - "Sort by title": "Nach Titel sortieren", - "Title": "Titel", - "Sort by time": "Nach Uhrzeit sortieren", - "Time": "Uhrzeit", - "Export history": "Verlauf exportieren", - "Import history": "Verlauf importieren", - "Clear history": "Verlauf löschen", - "Refresh history": "Verlauf aktualisieren", - "No history": "Kein Verlauf", - "Import from browser": "Vom Browser importieren", - "Releases": "Versionen", - "Are you sure?": "Sind sie sicher?", - "Do you really want to delete this note?": "Möchten Sie diese Notiz wirklich löschen?", - "All users will lose their connection.": "Alle Benutzer werden getrennt.", - "Cancel": "Abbrechen", - "Yes, do it!": "Ja, mach es!", - "Choose method": "Methode wählen", - "Sign in via %s": "Einloggen über %s", - "New": "Neu", - "Publish": "Veröffentlichen", - "Extra": "Extra", - "Revision": "Version", - "Slide Mode": "Präsentationsmodus", - "Export": "Exportieren", - "Import": "Importieren", - "Clipboard": "Zwischenablage", - "Download": "Download", - "Raw HTML": "Reines HTML", - "Edit": "Bearbeiten", - "View": "Anzeigen", - "Both": "Beides", - "Help": "Hilfe", - "Upload Image": "Foto hochladen", - "Menu": "Menü", - "This page need refresh": "Bitte laden Sie die Seite neu", - "You have an incompatible client version.": "Ihre Client Version ist nicht mit dem Server kompatibel", - "Refresh to update.": "Neu laden zum Updaten.", - "New version available!": "Neue Version verfügbar.", - "See releases notes here": "Versionshinweise", - "Refresh to enjoy new features.": "Neu laden für neue Funktionen", - "Your user state has changed.": "Ihr Nutzerstatus hat sich geändert.", - "Refresh to load new user state.": "Neu laden für neuen Nutzerstatus.", - "Refresh": "Neu laden", - "Contacts": "Kontakte", - "Report an issue": "Fehlerbericht senden", - "Meet us on %s": "Triff uns auf %s", - "Send us email": "Kontakt", - "Documents": "Dokumente", - "Features": "Funktionen", - "YAML Metadata": "YAML Metadaten", - "Slide Example": "Beispiel Präsentation", - "Cheatsheet": "Cheatsheet", - "Example": "Beispiel", - "Syntax": "Syntax", - "Header": "Überschrift", - "Unordered List": "Stichpunkte", - "Ordered List": "Nummeriert", - "Todo List": "To Do Liste", - "Blockquote": "Zitat", - "Bold font": "Fett", - "Italics font": "Kursiv", - "Strikethrough": "Durchgestrichen", - "Inserted text": "Unterstrichen", - "Marked text": "Markiert", - "Link": "Link", - "Image": "Foto", - "Code": "Code", - "Externals": "Extern", - "This is a alert area.": "Hinweisfeld", - "Revert": "Zurücksetzen", - "Import from clipboard": "Importieren aus der Zwischenablage", - "Paste your markdown or webpage here...": "Markdown oder Website hier einfügen", - "Clear": "Zurücksetzen", - "This note is locked": "Diese Notiz ist gesperrt", - "Sorry, only owner can edit this note.": "Entschuldigung, nur der Besitzer darf die Notiz bearbeiten.", - "OK": "Ok", - "Reach the limit": "Limit erreicht", - "Sorry, you've reached the max length this note can be.": "Entschuldigung, die maximale Länge der Notiz ist erreicht.", - "Please reduce the content or divide it to more notes, thank you!": "Bitte reduzieren Sie den Inhalt oder nutzen zwei Notizen, danke.", - "Import from Gist": "Aus GitHub Gist importieren", - "Paste your gist url here...": "gist URL hier einfügen ...", - "Import from Snippet": "Aus Snippet importieren", - "Select From Available Projects": "Aus verfügbaren Projekten wählen", - "Select From Available Snippets": "Aus verfügbaren Snippets wählen", - "OR": "Oder", - "Export to Snippet": "Zu Snippet exportieren", - "Select Visibility Level": "Sichtbarkeit bestimmen", - "Night Theme": "Nachtmodus", - "Follow us on %s and %s.": "Folge uns auf %s und %s." -} + "Collaborative markdown notes": "Gemeinschaftliche Markdown Notizen", + "Realtime collaborative markdown notes on all platforms.": "Gemeinschaftliche Notizen in Echtzeit auf allen Plattformen.", + "Best way to write and share your knowledge in markdown.": "Der beste Weg, Notizen zu schreiben und teilen.", + "Intro": "Intro", + "History": "Verlauf", + "New guest note": "Neue Gast Notiz", + "Collaborate with URL": "Zusammenarbeiten mit URL", + "Support charts and MathJax": "Unterstützt charts und Mathjax", + "Support slide mode": "Unterstützt Präsentationsmodus", + "Sign In": "Einloggen", + "Below is the history from browser": "Lokaler Browserverlauf", + "Welcome!": "Willkommen!", + "New note": "Neue Notiz", + "or": "oder", + "Sign Out": "Ausloggen", + "Explore all features": "Alle Funktionen", + "Select tags...": "Tags auswählen ...", + "Search keyword...": "Suche nach Stichwort ...", + "Sort by title": "Nach Titel sortieren", + "Title": "Titel", + "Sort by time": "Nach Uhrzeit sortieren", + "Time": "Uhrzeit", + "Export history": "Verlauf exportieren", + "Import history": "Verlauf importieren", + "Clear history": "Verlauf löschen", + "Refresh history": "Verlauf aktualisieren", + "No history": "Kein Verlauf", + "Import from browser": "Vom Browser importieren", + "Releases": "Versionen", + "Are you sure?": "Sind sie sicher?", + "Do you really want to delete this note?": "Möchten Sie diese Notiz wirklich löschen?", + "All users will lose their connection.": "Alle Benutzer werden getrennt.", + "Cancel": "Abbrechen", + "Yes, do it!": "Ja, mach es!", + "Choose method": "Methode wählen", + "Sign in via %s": "Einloggen über %s", + "New": "Neu", + "Publish": "Veröffentlichen", + "Extra": "Extra", + "Revision": "Version", + "Slide Mode": "Präsentationsmodus", + "Export": "Exportieren", + "Import": "Importieren", + "Clipboard": "Zwischenablage", + "Download": "Download", + "Raw HTML": "Reines HTML", + "Edit": "Bearbeiten", + "View": "Anzeigen", + "Both": "Beides", + "Help": "Hilfe", + "Upload Image": "Foto hochladen", + "Menu": "Menü", + "This page need refresh": "Bitte laden Sie die Seite neu", + "You have an incompatible client version.": "Ihre Client Version ist nicht mit dem Server kompatibel", + "Refresh to update.": "Neu laden zum Updaten.", + "New version available!": "Neue Version verfügbar.", + "See releases notes here": "Versionshinweise", + "Refresh to enjoy new features.": "Neu laden für neue Funktionen", + "Your user state has changed.": "Ihr Nutzerstatus hat sich geändert.", + "Refresh to load new user state.": "Neu laden für neuen Nutzerstatus.", + "Refresh": "Neu laden", + "Contacts": "Kontakte", + "Report an issue": "Fehlerbericht senden", + "Meet us on %s": "Triff uns auf %s", + "Send us email": "Kontakt", + "Documents": "Dokumente", + "Features": "Funktionen", + "YAML Metadata": "YAML Metadaten", + "Slide Example": "Beispiel Präsentation", + "Cheatsheet": "Cheatsheet", + "Example": "Beispiel", + "Syntax": "Syntax", + "Header": "Überschrift", + "Unordered List": "Stichpunkte", + "Ordered List": "Nummeriert", + "Todo List": "To Do Liste", + "Blockquote": "Zitat", + "Bold font": "Fett", + "Italics font": "Kursiv", + "Strikethrough": "Durchgestrichen", + "Inserted text": "Unterstrichen", + "Marked text": "Markiert", + "Link": "Link", + "Image": "Foto", + "Code": "Code", + "Externals": "Extern", + "This is a alert area.": "Hinweisfeld", + "Revert": "Zurücksetzen", + "Import from clipboard": "Importieren aus der Zwischenablage", + "Paste your markdown or webpage here...": "Markdown oder Website hier einfügen", + "Clear": "Zurücksetzen", + "This note is locked": "Diese Notiz ist gesperrt", + "Sorry, only owner can edit this note.": "Entschuldigung, nur der Besitzer darf die Notiz bearbeiten.", + "OK": "Ok", + "Reach the limit": "Limit erreicht", + "Sorry, you've reached the max length this note can be.": "Entschuldigung, die maximale Länge der Notiz ist erreicht.", + "Please reduce the content or divide it to more notes, thank you!": "Bitte reduzieren Sie den Inhalt oder nutzen zwei Notizen, danke.", + "Import from Gist": "Aus GitHub Gist importieren", + "Paste your gist url here...": "gist URL hier einfügen ...", + "Import from Snippet": "Aus Snippet importieren", + "Select From Available Projects": "Aus verfügbaren Projekten wählen", + "Select From Available Snippets": "Aus verfügbaren Snippets wählen", + "OR": "Oder", + "Export to Snippet": "Zu Snippet exportieren", + "Select Visibility Level": "Sichtbarkeit bestimmen", + "Night Theme": "Nachtmodus", + "Follow us on %s and %s.": "Folge uns auf %s und %s.", + "Privacy": "Privatsphäre", + "Terms of Use": "Nutzungsbedingungen", + "Do you really want to delete your user account?": "Möchten Sie wirklich Ihr Nutzeraccount löschen?", + "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Hiermit löschen Sie Ihren Account, alle Ihre Dokumente und alle Verweise auf Ihren Account aus anderen Dokumenten.", + "Delete user": "Benutzer löschen", + "Export user data": "Exportiere Nutzerdaten" +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 1aef3f6..f9c29b5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -105,5 +105,11 @@ "Export to Snippet": "Export to Snippet", "Select Visibility Level": "Select Visibility Level", "Night Theme": "Night Theme", - "Follow us on %s and %s.": "Follow us on %s, and %s." + "Follow us on %s and %s.": "Follow us on %s, and %s.", + "Privacy": "Privacy", + "Terms of Use": "Terms of Use", + "Do you really want to delete your user account?": "Do you really want to delete your user account?", + "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.", + "Delete user": "Delete user", + "Export user data": "Export user data" } diff --git a/locales/it.json b/locales/it.json index 5c97208..207f3e7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,104 +1,115 @@ { - "Collaborative markdown notes": "Note collaborative in markdown", - "Realtime collaborative markdown notes on all platforms.": "Note markdown collaborative per tutte le piattaforme.", - "Best way to write and share your knowledge in markdown.": "Miglior modo per scrivere e condividere le tue conoscenze in markdown.", - "Intro": "Intro", - "History": "Cronologia", - "New guest note": "Nuova nota ospite", - "Collaborate with URL": "Collabora tramite URL", - "Support charts and MathJax": "Supporta grafici e MathJax", - "Support slide mode": "Supporta slide", - "Sign In": "Registrati", - "Below is the history from browser": "Below is the history from browser", - "Welcome!": "Benvenuto!", - "New note": "Nuova nota", - "or": "o", - "Sign Out": "Disconettiti", - "Explore all features": "Escplora tutte le funzioni", - "Select tags...": "Seleziona tag...", - "Search keyword...": "Cerca...", - "Sort by title": "Ordina per titolo", - "Title": "Titolo", - "Sort by time": "Ordina per data", - "Time": "Data", - "Export history": "Esporta cronologia", - "Import history": "Importa cronologia", - "Clear history": "Cancella cronologia", - "Refresh history": "Aggiorna cronologia", - "No history": "Nessuna cronologia", - "Import from browser": "Importa da browser", - "Releases": "Versioni", - "Are you sure?": "Sei sicuro?", - "Cancel": "Cancella", - "Yes, do it!": "SI, fallo!", - "Choose method": "Scegli metodo", - "Sign in via %s": "Registrati con %s", - "New": "Nuovo", - "Publish": "Pubblica", - "Extra": "Extra", - "Revision": "Revisione", - "Slide Mode": "Modalità slide", - "Export": "Esporta", - "Import": "Importa", - "Clipboard": "Appunti", - "Download": "Scarica", - "Raw HTML": "Raw HTML", - "Edit": "Modifica", - "View": "Visualizza", - "Both": "Entrambi", - "Help": "Aiuto", - "Upload Image": "Carica Immagine", - "Menu": "Menu", - "This page need refresh": "Questa pagina deve essere aggiornata", - "You have an incompatible client version.": "Hai una versione incompatibile.", - "Refresh to update.": "Ricarica per aggiornare.", - "New version available!": "Nuova versione disponibile!", - "See releases notes here": "Vedi note di rilascio qui", - "Refresh to enjoy new features.": "Ricarica per godere delle nuove funzioni.", - "Your user state has changed.": "Il tuo stato utente è cambiato.", - "Refresh to load new user state.": "Aggiorna per caricare il nuovo stato utente.", - "Refresh": "Ricarica", - "Contacts": "Contatti", - "Report an issue": "Segnala un problema", - "Send us email": "Inviaci una email", - "Documents": "Documenti", - "Features": "Caratteristiche", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Esempio Slide", - "Cheatsheet": "Cheatsheet", - "Example": "Esempio", - "Syntax": "Sintassi", - "Header": "Intestazione", - "Unordered List": "Lista non ordinata", - "Ordered List": "Lista ordinata", - "Todo List": "Elenco", - "Blockquote": "Blockquote", - "Bold font": "Grassetto", - "Italics font": "Corsivo", - "Strikethrough": "Barrato", - "Inserted text": "Sottolineato", - "Marked text": "Evidenziato", - "Link": "Link", - "Image": "Immagine", - "Code": "Codice", - "Externals": "Esterno", - "This is a alert area.": "Questa è un avviso.", - "Revert": "Revert", - "Import from clipboard": "Imposta dagli Appunti", - "Paste your markdown or webpage here...": "Incollare il markdown o una pagina web qui...", - "Clear": "Pulisci", - "This note is locked": "Questa nota è bloccata", - "Sorry, only owner can edit this note.": "Siamo spiacenti, solo il proprietario può modificare questa nota.", - "OK": "OK", - "Reach the limit": "Limite raggiunto", - "Sorry, you've reached the max length this note can be.": "Siamo spiacenti, hai raggiunto la lunghezza massima per questa nota.", - "Please reduce the content or divide it to more notes, thank you!": "Si prega di ridurre il contenuto o dividerlo in più note, grazie!", - "Import from Gist": "Importa da Gist", - "Paste your gist url here...": "Incolla il tuo link gist qui...", - "Import from Snippet": "Importa da Snippet", - "Select From Available Projects": "Seleziona da progetti disponibili", - "Select From Available Snippets": "Seleziona da snippets disponibili", - "OR": "O", - "Export to Snippet": "Esporta Snippet", - "Select Visibility Level": "Seleziona livello visibilità" -} + "Collaborative markdown notes": "Note collaborative in markdown", + "Realtime collaborative markdown notes on all platforms.": "Note markdown collaborative in tempo reale per tutte le piattaforme.", + "Best way to write and share your knowledge in markdown.": "Miglior modo per scrivere e condividere le tue conoscenze in markdown.", + "Intro": "Intro", + "History": "Cronologia", + "New guest note": "Nuova nota ospite", + "Collaborate with URL": "Collabora tramite URL", + "Support charts and MathJax": "Supporta grafici e MathJax", + "Support slide mode": "Supporta modalità slide", + "Sign In": "Registrati", + "Below is the history from browser": "Qui sotto c'è la cronologia del browser", + "Welcome!": "Benvenuto!", + "New note": "Nuova nota", + "or": "o", + "Sign Out": "Disconettiti", + "Explore all features": "Esplora tutte le funzioni", + "Select tags...": "Seleziona tag...", + "Search keyword...": "Cerca...", + "Sort by title": "Ordina per titolo", + "Title": "Titolo", + "Sort by time": "Ordina per data", + "Time": "Data", + "Export history": "Esporta cronologia", + "Import history": "Importa cronologia", + "Clear history": "Cancella cronologia", + "Refresh history": "Aggiorna cronologia", + "No history": "Nessuna cronologia", + "Import from browser": "Importa da browser", + "Releases": "Versioni", + "Are you sure?": "Sei sicuro?", + "Do you really want to delete this note?": "Vuoi veramente eliminare questa nota?", + "All users will lose their connection.": "Tutti gli utenti perderanno la loro connessione.", + "Cancel": "Annulla", + "Yes, do it!": "SI, fallo!", + "Choose method": "Scegli metodo", + "Sign in via %s": "Registrati con %s", + "New": "Nuovo", + "Publish": "Pubblica", + "Extra": "Extra", + "Revision": "Revisione", + "Slide Mode": "Modalità slide", + "Export": "Esporta", + "Import": "Importa", + "Clipboard": "Appunti", + "Download": "Scarica", + "Raw HTML": "Raw HTML", + "Edit": "Modifica", + "View": "Visualizza", + "Both": "Entrambi", + "Help": "Aiuto", + "Upload Image": "Carica Immagine", + "Menu": "Menu", + "This page need refresh": "Questa pagina deve essere aggiornata", + "You have an incompatible client version.": "La versione del tuo client è incompatibile.", + "Refresh to update.": "Ricarica per aggiornare.", + "New version available!": "Nuova versione disponibile!", + "See releases notes here": "Vedi note di rilascio qui", + "Refresh to enjoy new features.": "Ricarica per godere delle nuove funzioni.", + "Your user state has changed.": "Il tuo stato utente è cambiato.", + "Refresh to load new user state.": "Aggiorna per caricare il nuovo stato utente.", + "Refresh": "Ricarica", + "Contacts": "Contatti", + "Report an issue": "Segnala un problema", + "Meet us on %s": "Vieni a trovarci su %s", + "Send us email": "Inviaci una email", + "Documents": "Documenti", + "Features": "Caratteristiche", + "YAML Metadata": "YAML Metadata", + "Slide Example": "Esempio Slide", + "Cheatsheet": "Cheatsheet", + "Example": "Esempio", + "Syntax": "Sintassi", + "Header": "Intestazione", + "Unordered List": "Lista non ordinata", + "Ordered List": "Lista ordinata", + "Todo List": "Elenco", + "Blockquote": "Citazione", + "Bold font": "Grassetto", + "Italics font": "Corsivo", + "Strikethrough": "Barrato", + "Inserted text": "Sottolineato", + "Marked text": "Evidenziato", + "Link": "Link", + "Image": "Immagine", + "Code": "Codice", + "Externals": "Esterni", + "This is a alert area.": "Questa è un area di avviso.", + "Revert": "Annulla", + "Import from clipboard": "Importa dagli appunti", + "Paste your markdown or webpage here...": "Incollare il markdown o una pagina web qui...", + "Clear": "Pulisci", + "This note is locked": "Questa nota è bloccata", + "Sorry, only owner can edit this note.": "Siamo spiacenti, solo il proprietario può modificare questa nota.", + "OK": "OK", + "Reach the limit": "Limite raggiunto", + "Sorry, you've reached the max length this note can be.": "Siamo spiacenti, hai raggiunto la lunghezza massima per questa nota.", + "Please reduce the content or divide it to more notes, thank you!": "Si prega di ridurre il contenuto o dividerlo in più note, grazie!", + "Import from Gist": "Importa da Gist", + "Paste your gist url here...": "Incolla il tuo link gist qui...", + "Import from Snippet": "Importa da Snippet", + "Select From Available Projects": "Seleziona da progetti disponibili", + "Select From Available Snippets": "Seleziona da snippets disponibili", + "OR": "O", + "Export to Snippet": "Esporta Snippet", + "Select Visibility Level": "Seleziona livello visibilità", + "Night Theme": "Tema Scuro", + "Follow us on %s and %s.": "Seguici su %s, e %s.", + "Privacy": "Privacy", + "Terms of Use": "Termini di Utilizzo", + "Do you really want to delete your user account?": "Vuoi veramente cancellare il tuo account utente?", + "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Questo cancellerà il tuo account, tutte le note di cui sei proprietario e rimuoverà i riferimenti al tuo account dalle altre note.", + "Delete user": "Elimina utente", + "Export user data": "Esporta dati utente" +} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json index fb3de58..80623c8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,104 +1,115 @@ { - "Collaborative markdown notes": "共同編集できるMarkdownノート", - "Realtime collaborative markdown notes on all platforms.": "マルチプラットフォーム、リアルタイムで共同編集できるMarkdownノート", - "Best way to write and share your knowledge in markdown.": "Markdownでナレッジを蓄積・共有できるベストツール", - "Intro": "サービスの紹介", - "History": "履歴", - "New guest note": "新規ゲストノート", - "Collaborate with URL": "URLで共同編集", - "Support charts and MathJax": "グラフとMathJaxのサポート", - "Support slide mode": "スライドモードのサポート", - "Sign In": "サインイン", - "Below is the history from browser": "ブラウザからの履歴", - "Welcome!": "ようこそ!", - "New note": "新規ノート", - "or": "または", - "Sign Out": "サインアウト", - "Explore all features": "すべての機能をチェック", - "Select tags...": "タグで検索", - "Search keyword...": "キーワードで検索", - "Sort by title": "タイトル順でソート", - "Title": "タイトル", - "Sort by time": "日時順でソート", - "Time": "日時", - "Export history": "履歴をエクスポート", - "Import history": "履歴をインポート", - "Clear history": "履歴をクリア", - "Refresh history": "履歴を更新", - "No history": "履歴はありません", - "Import from browser": "ブラウザからインポート", - "Releases": "リリース", - "Are you sure?": "本当にいいですか?", - "Cancel": "キャンセル", - "Yes, do it!": "はい", - "Choose method": "選択してください", - "Sign in via %s": "%sでサインイン", - "New": "新規作成", - "Publish": "公開する", - "Extra": "その他", - "Revision": "編集履歴", - "Slide Mode": "スライドモード", - "Export": "エクスポート", - "Import": "インポート", - "Clipboard": "クリップボード", - "Download": "ダウンロード", - "Raw HTML": "HTMLパーツ", - "Edit": "編集モード", - "View": "表示モード", - "Both": "分割モード", - "Help": "ヘルプ", - "Upload Image": "画像をアップロード", - "Menu": "メニュー", - "This page need refresh": "ページをリロードしてください", - "You have an incompatible client version.": "クライアントのバージョンが一致しません", - "Refresh to update.": "リロードして更新を反映させてください", - "New version available!": "新しいバージョンが利用できます!", - "See releases notes here": "リリースノートをごらんください", - "Refresh to enjoy new features.": "リロードして新しい機能を試してみましょう", - "Your user state has changed.": "ユーザー情報が変更されました", - "Refresh to load new user state.": "リロードすると最新のユーザー情報が反映されます", - "Refresh": "リロード", - "Contacts": "コンタクト", - "Report an issue": "問題を報告する", - "Send us email": "メールを送る", - "Documents": "ドキュメント", - "Features": "機能", - "YAML Metadata": "YAMLメタデータ", - "Slide Example": "スライドサンプル", - "Cheatsheet": "チートシート", - "Example": "例", - "Syntax": "構文", - "Header": "見出し", - "Unordered List": "番号なしリスト", - "Ordered List": "番号付きリスト", - "Todo List": "TODOリスト", - "Blockquote": "引用文", - "Bold font": "太字", - "Italics font": "斜体", - "Strikethrough": "打ち消し線", - "Inserted text": "挿入文", - "Marked text": "マーカー", - "Link": "リンク", - "Image": "画像", - "Code": "コード", - "Externals": "モジュール", - "This is a alert area.": "これはアラートエリアです", - "Revert": "戻す", - "Import from clipboard": "クリップボードからインポート", - "Paste your markdown or webpage here...": "Markdownまたはウェブページを貼り付けてください", - "Clear": "クリア", - "This note is locked": "このノートはロックされています", - "Sorry, only owner can edit this note.": "このノートはオーナーのみが編集できます", - "OK": "OK", - "Reach the limit": "上限に達しました", - "Sorry, you've reached the max length this note can be.": "ノートの文字数が上限に達しました。", - "Please reduce the content or divide it to more notes, thank you!": "内容を減らすか、別のノートに分けてください", - "Import from Gist": "gistからインポート", - "Paste your gist url here...": "gistのURLを貼り付けてください", - "Import from Snippet": "スニペットからインポート", - "Select From Available Projects": "プロジェクトを一覧から選択してください", - "Select From Available Snippets": "スニペットを一覧から選択してください", - "OR": "または", - "Export to Snippet": "スニペットにエクスポート", - "Select Visibility Level": "公開範囲を選んでください" -} + "Collaborative markdown notes": "共同編集できるMarkdownノート", + "Realtime collaborative markdown notes on all platforms.": "マルチプラットフォーム、リアルタイムで共同編集できるMarkdownノート", + "Best way to write and share your knowledge in markdown.": "Markdownでナレッジを蓄積・共有できるベストツール", + "Intro": "サービスの紹介", + "History": "履歴", + "New guest note": "新規ゲストノート", + "Collaborate with URL": "URLで共同編集", + "Support charts and MathJax": "グラフとMathJaxのサポート", + "Support slide mode": "スライドモードのサポート", + "Sign In": "サインイン", + "Below is the history from browser": "ブラウザからの履歴", + "Welcome!": "ようこそ!", + "New note": "新規ノート", + "or": "または", + "Sign Out": "サインアウト", + "Explore all features": "すべての機能をチェック", + "Select tags...": "タグで検索", + "Search keyword...": "キーワードで検索", + "Sort by title": "タイトル順でソート", + "Title": "タイトル", + "Sort by time": "日時順でソート", + "Time": "日時", + "Export history": "履歴をエクスポート", + "Import history": "履歴をインポート", + "Clear history": "履歴をクリア", + "Refresh history": "履歴を更新", + "No history": "履歴はありません", + "Import from browser": "ブラウザからインポート", + "Releases": "リリース", + "Are you sure?": "本当にいいですか?", + "Do you really want to delete this note?": "本当にこのノートを削除しますか?", + "All users will lose their connection.": "すべてのユーザーの接続が切断されます。", + "Cancel": "キャンセル", + "Yes, do it!": "はい", + "Choose method": "選択してください", + "Sign in via %s": "%sでサインイン", + "New": "新規作成", + "Publish": "公開する", + "Extra": "その他", + "Revision": "編集履歴", + "Slide Mode": "スライドモード", + "Export": "エクスポート", + "Import": "インポート", + "Clipboard": "クリップボード", + "Download": "ダウンロード", + "Raw HTML": "HTMLパーツ", + "Edit": "編集モード", + "View": "表示モード", + "Both": "分割モード", + "Help": "ヘルプ", + "Upload Image": "画像をアップロード", + "Menu": "メニュー", + "This page need refresh": "ページをリロードしてください", + "You have an incompatible client version.": "クライアントのバージョンが一致しません", + "Refresh to update.": "リロードして更新を反映させてください", + "New version available!": "新しいバージョンが利用できます!", + "See releases notes here": "リリースノートをごらんください", + "Refresh to enjoy new features.": "リロードして新しい機能を試してみましょう", + "Your user state has changed.": "ユーザー情報が変更されました", + "Refresh to load new user state.": "リロードすると最新のユーザー情報が反映されます", + "Refresh": "リロード", + "Contacts": "コンタクト", + "Report an issue": "問題を報告する", + "Meet us on %s": "%sでチャットする", + "Send us email": "メールを送る", + "Documents": "ドキュメント", + "Features": "機能", + "YAML Metadata": "YAMLメタデータ", + "Slide Example": "スライドサンプル", + "Cheatsheet": "チートシート", + "Example": "例", + "Syntax": "構文", + "Header": "見出し", + "Unordered List": "番号なしリスト", + "Ordered List": "番号付きリスト", + "Todo List": "TODOリスト", + "Blockquote": "引用文", + "Bold font": "太字", + "Italics font": "斜体", + "Strikethrough": "打ち消し線", + "Inserted text": "挿入文", + "Marked text": "マーカー", + "Link": "リンク", + "Image": "画像", + "Code": "コード", + "Externals": "モジュール", + "This is a alert area.": "これはアラートエリアです", + "Revert": "戻す", + "Import from clipboard": "クリップボードからインポート", + "Paste your markdown or webpage here...": "Markdownまたはウェブページを貼り付けてください", + "Clear": "クリア", + "This note is locked": "このノートはロックされています", + "Sorry, only owner can edit this note.": "このノートはオーナーのみが編集できます", + "OK": "OK", + "Reach the limit": "上限に達しました", + "Sorry, you've reached the max length this note can be.": "ノートの文字数が上限に達しました。", + "Please reduce the content or divide it to more notes, thank you!": "内容を減らすか、別のノートに分けてください", + "Import from Gist": "gistからインポート", + "Paste your gist url here...": "gistのURLを貼り付けてください", + "Import from Snippet": "スニペットからインポート", + "Select From Available Projects": "プロジェクトを一覧から選択してください", + "Select From Available Snippets": "スニペットを一覧から選択してください", + "OR": "または", + "Export to Snippet": "スニペットにエクスポート", + "Select Visibility Level": "公開範囲を選んでください", + "Night Theme": "ナイトテーマ", + "Follow us on %s and %s.": "%s と %s でフォローしてください。", + "Privacy": "プライバシー", + "Terms of Use": "利用条件", + "Do you really want to delete your user account?": "本当にアカウントを削除しますか?", + "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "この操作はあなたのアカウントとあなたの所有するすべてのノートを削除し、さらに他の人のノートからあなたのアカウントへの参照を除去します。", + "Delete user": "ユーザーの削除", + "Export user data": "ユーザーデータをエクスポート" +} \ No newline at end of file diff --git a/locales/zh.json b/locales/zh.json deleted file mode 120000 index 77c7eac..0000000 --- a/locales/zh.json +++ /dev/null @@ -1 +0,0 @@ -locales/zh-TW.json \ No newline at end of file diff --git a/package.json b/package.json index b7420ce..1740500 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "hackmd", - "version": "1.1.1-ce", + "name": "CodiMD", + "version": "1.2.0", "description": "Realtime collaborative markdown notes on all platforms.", "main": "app.js", "license": "AGPL-3.0", @@ -16,6 +16,7 @@ }, "dependencies": { "Idle.Js": "git+https://github.com/shawnmclean/Idle.js", + "archiver": "^2.1.1", "async": "^2.1.4", "aws-sdk": "^2.7.20", "base64url": "^3.0.0", @@ -46,7 +47,7 @@ "graceful-fs": "^4.1.11", "handlebars": "^4.0.6", "helmet": "^3.3.0", - "highlight.js": "~9.9.0", + "highlight.js": "~9.12.0", "i18n": "^0.8.3", "imgur": "git+https://github.com/hackmdio/node-imgur.git", "ionicons": "~2.0.1", @@ -75,26 +76,26 @@ "markdown-it-regexp": "^0.4.0", "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", - "markdown-pdf": "^7.0.0", + "markdown-pdf": "^9.0.0", "mathjax": "~2.7.0", "mermaid": "~7.1.0", "mattermost": "^3.4.0", "meta-marked": "^0.4.2", "method-override": "^2.3.7", "minimist": "^1.2.0", - "minio": "^3.1.3", + "minio": "^6.0.0", "moment": "^2.17.1", "morgan": "^1.7.0", "mysql": "^2.12.0", "node-uuid": "^1.4.7", - "octicons": "~3.5.0", - "passport": "^0.3.2", + "octicons": "~4.4.0", + "passport": "^0.4.0", "passport-dropbox-oauth2": "^1.1.0", "passport-facebook": "^2.1.1", "passport-github": "^1.1.0", - "passport-gitlab2": "^2.2.0", + "passport-gitlab2": "^4.0.0", "passport-google-oauth20": "^1.0.0", - "passport-ldapauth": "^0.6.0", + "passport-ldapauth": "^2.0.0", "passport-local": "^1.0.0", "passport-oauth2": "^1.4.0", "passport-twitter": "^1.0.4", @@ -104,7 +105,7 @@ "pg": "^6.1.2", "pg-hstore": "^2.3.2", "prismjs": "^1.6.0", - "randomcolor": "^0.4.4", + "randomcolor": "^0.5.3", "raphael": "git+https://github.com/dmitrybaranovskiy/raphael", "readline-sync": "^1.4.7", "request": "^2.79.0", @@ -113,29 +114,29 @@ "select2": "^3.5.2-browserify", "sequelize": "^3.28.0", "sequelize-cli": "^2.5.1", - "shortid": "2.2.6", + "shortid": "2.2.8", "socket.io": "~2.0.4", "socket.io-client": "~2.0.4", "spin.js": "^2.3.2", - "sqlite3": "^3.1.8", - "store": "^1.3.20", + "sqlite3": "^4.0.1", + "store": "^2.0.12", "string": "^3.3.3", "tedious": "^1.14.0", "to-markdown": "^3.0.3", "toobusy-js": "^0.5.1", "uuid": "^3.1.0", "uws": "~0.14.1", - "validator": "^6.2.0", + "validator": "^10.4.0", "velocity-animate": "^1.4.0", "visibilityjs": "^1.2.4", "viz.js": "^1.7.0", "winston": "^2.3.0", - "xss": "^0.3.3" + "xss": "^1.0.3" }, "engines": { "node": ">=6.x <10.x" }, - "bugs": "https://github.com/hackmdio/hackmd/issues", + "bugs": "https://github.com/hackmdio/codimd/issues", "keywords": [ "Collaborative", "Markdown", @@ -150,7 +151,7 @@ ], "repository": { "type": "git", - "url": "https://github.com/hackmdio/hackmd.git" + "url": "https://github.com/hackmdio/codimd.git" }, "devDependencies": { "babel-cli": "^6.18.0", diff --git a/public/hackmd-icon-1024.png b/public/codimd-icon-1024.png similarity index 100% rename from public/hackmd-icon-1024.png rename to public/codimd-icon-1024.png diff --git a/public/css/extra.css b/public/css/extra.css index 1b13290..3954c04 100644 --- a/public/css/extra.css +++ b/public/css/extra.css @@ -93,6 +93,10 @@ color: #777; } +.toc .invisable-node { + list-style-type: none; +} + .ui-toc { position: fixed; bottom: 20px; diff --git a/public/css/index.css b/public/css/index.css index 3f391e2..d9ebf6d 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -20,6 +20,24 @@ body.night{ background: #333 !important; } +.toolbar { + background-color: #1c1c1e; + border: 1px solid #343434; +} + +.toolbar > .btn-toolbar > .btn-group > .btn { + background-color: #1c1c1e; + padding: 5px; + font-size: 1em; +} + +.toolbar > .btn-toolbar > .btn-group > .btn:hover { + background-color: #383a3e; + + padding: 5px; +} + + .CodeMirror { font-family: "Source Code Pro", Consolas, monaco, monospace; letter-spacing: 0.025em; @@ -292,7 +310,16 @@ body.night{ overflow: hidden; } -.night .dropdown-menu{ +.night .dropdown-menu > li > a { + color: #eee; +} + +.night .dropdown-menu > li > a:focus, +.night .dropdown-menu > li > a:hover { + color: #262626 +} + +.night .dropdown-menu { background: #222; } diff --git a/public/css/markdown.css b/public/css/markdown.css index 85a4c59..23ed6ba 100644 --- a/public/css/markdown.css +++ b/public/css/markdown.css @@ -214,3 +214,7 @@ .markdown-body figure { margin: 1em 40px; } + +.markdown-body img { + background-color: transparent; +} diff --git a/public/docs/features.md b/public/docs/features.md index dc6ddaf..f684ca6 100644 --- a/public/docs/features.md +++ b/public/docs/features.md @@ -3,12 +3,12 @@ Features Introduction === - **HackMD** is a realtime, multi-platform collaborative markdown note editor. + **CodiMD** is a real-time, multi-platform collaborative markdown note editor. This means that you can write notes with other people on your **desktop**, **tablet** or even on the **phone**. You can sign-in via multiple auth providers like **Facebook**, **Twitter**, **GitHub** and many more on the [_homepage_](/). -If you experience any _issues_, feel free to report it on [**GitHub**](https://github.com/hackmdio/hackmd/issues). -Or meet us on [**Matrix.org**](https://riot.im/app/#/room/#hackmd:matrix.org) or [**Gitter**](https://gitter.im/hackmdio/hackmd) for dev-talk and interactive help. +If you experience any _issues_, feel free to report it on [**GitHub**](https://github.com/hackmdio/codimd/issues). +Or meet us on [**Matrix.org**](https://riot.im/app/#/room/#codimd:matrix.org) or [**Gitter**](https://gitter.im/hackmdio/hackmd) for dev-talk and interactive help. **Thank you very much!** Workspace @@ -26,7 +26,7 @@ Workspace Edit: See only the editor. ## Night Mode: -When you are tired of a white screen and like a night mode, click on the little moon and turn on the night view of HackMD. +When you are tired of a white screen and like a night mode, click on the little moon and turn on the night view of CodiMD. The editor view, which is in night mode by default, can also be toggled between night and day view using the the little sun. diff --git a/public/docs/privacy.md.example b/public/docs/privacy.md.example new file mode 100644 index 0000000..ff65d82 --- /dev/null +++ b/public/docs/privacy.md.example @@ -0,0 +1,17 @@ +Privacy +=== + +We process the following data, for the following purposes: + +|your data|our usage| +|---------|---------| +|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.| +|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.| +|Profile pictures| Your profile picture is either loaded from the service you used to login, the CodiMD instance or Gravatar.| +|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.| + +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. + +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. + +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. diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index 891c506..6620200 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -1,6 +1,54 @@ Release Notes === + 1.2.0 2018-06-28 00:00 +--- + +### Announcement +* HackMD CE is renamed to CodiMD to prevent confusion. [For details see here](https://github.com/hackmdio/codimd#hackmd-ce-became-codimd) + +### Enhancements +* Show full title by hovering over to table of contents entries +* Add generic OAUTH2 support for authentication +* Redirect unauthenticated user to login page on "forbidden" pages +* Add ability to add ToS and privacy documents without code changes +* Add account deletion as part of user self-management +* Add download of all own notes +* Add privacy policy example (no legal advice) +* Increase checkbox size on slides +* Add support for Azure blob storage for image uploads +* Add Korean translation +* Add note about official K8s chart for deployment +* Add toolbar for markdown shortcuts in editor +* Add ability to disable Gravatar integration +* Add print icon to slide menu which leads to the print view. +* Add sequelize to setup instructions +* Update various packages + +### Fixes +* Fix local writes for non-existing translations in production +* Fix wrong documentation about default image upload type +* Fix possible error if CodiMD is started with wrong working directory +* Fix issues caused by cached/cacheeable client config +* Fix issues caused by notes created via curl/API with CRLF line endings +* Fix broken images for downloaded PDFs while using `filesystem` as `imageUploadType` +* Fix Unicode URLs when using `allowFreeURL=true` + +### Refactors +* Split auth documentation into multiple documents + +### Removes +* Remove polyfill for `useCDN=false` setups +* Remove unused and no longer needed symlink from translations + +### Honorable mentions +* [Adam Hoka (ahoka)](https://github.com/ahoka) +* [Edgar Z. Alvarenga (aivuk)](https://github.com/aivuk) +* [Jacob Burden (jekrb)](https://github.com/jekrb) +* [Pedro Ferreira (pferreir)](https://github.com/pferreir) +* [TC Liu (liuderchi)](https://github.com/liuderchi) + + 1.1.1-ce 2018-05-23 12:00 --- diff --git a/public/docs/slide-example.md b/public/docs/slide-example.md index e2e024b..411f31d 100644 --- a/public/docs/slide-example.md +++ b/public/docs/slide-example.md @@ -266,4 +266,12 @@ Press `B` or `.` on your keyboard to pause the presentation. This is helpful whe --- +## Print your Slides + +Down below you can find a print icon. + +After you click on it, use the print function of your browser (either CTRL+P or cmd+P) to print the slides as PDF. + +--- + # The End diff --git a/public/docs/yaml-metadata.md b/public/docs/yaml-metadata.md index d49418f..888345f 100644 --- a/public/docs/yaml-metadata.md +++ b/public/docs/yaml-metadata.md @@ -96,7 +96,7 @@ dir: rtl breaks --- This option means the hardbreaks in the note will be parsed or be ignore. -The original markdown syntax breaks only if you put space twice, but HackMD choose to breaks every time you enter a break. +The original markdown syntax breaks only if you put space twice, but CodiMD choose to breaks every time you enter a break. You can only use whether `true` or `false`. > default: not set (which will be true) @@ -125,7 +125,7 @@ This option allow you to enable Disqus with your shortname. **Example** ```xml -disqus: hackmd +disqus: codimd ``` slideOptions @@ -146,4 +146,4 @@ https://github.com/hakimel/reveal.js/tree/master/css/theme slideOptions: transition: fade theme: white -``` \ No newline at end of file +``` diff --git a/public/js/cover.js b/public/js/cover.js index c9c2b6c..79fb3a2 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -34,25 +34,25 @@ import S from 'string' const options = { valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'], - item: '
  • ' + - '' + - '' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '

    ' + - '

    ' + - ' visited ' + - '
    ' + - '' + - '' + - '

    ' + - '

    ' + - '
    ' + - '
    ' + - '
    ' + - '
  • ', + item: `
  • + + +
    +
    +
    +
    +

    +

    + visited +
    + + +

    +

    +
    +
    +
    +
  • `, page: 18, pagination: [{ outerWindow: 1 @@ -208,8 +208,8 @@ function historyCloseClick (e) { e.preventDefault() const id = $(this).closest('a').siblings('span').html() const value = historyList.get('id', id)[0]._values - $('.ui-delete-modal-msg').text('Do you really want to delete below history?') - $('.ui-delete-modal-item').html(` ${value.text}
    ${value.time}`) + $('.ui-delete-history-modal-msg').text('Do you really want to delete below history?') + $('.ui-delete-history-modal-item').html(` ${value.text}
    ${value.time}`) clearHistory = false deleteId = id } @@ -277,7 +277,7 @@ function deleteHistory () { checkHistoryList() } } - $('.delete-modal').modal('hide') + $('.delete-history-modal').modal('hide') deleteId = null clearHistory = false }) @@ -297,12 +297,12 @@ function deleteHistory () { deleteId = null }) } - $('.delete-modal').modal('hide') + $('.delete-history-modal').modal('hide') clearHistory = false }) } -$('.ui-delete-modal-confirm').click(() => { +$('.ui-delete-history-modal-confirm').click(() => { deleteHistory() }) @@ -318,7 +318,7 @@ $('.ui-save-history').click(() => { const blob = new Blob([history], { type: 'application/json;charset=utf-8' }) - saveAs(blob, `hackmd_history_${moment().format('YYYYMMDDHHmmss')}`, true) + saveAs(blob, `codimd_history_${moment().format('YYYYMMDDHHmmss')}`, true) }) }) @@ -342,8 +342,8 @@ $('.ui-open-history').bind('change', e => { }) $('.ui-clear-history').click(() => { - $('.ui-delete-modal-msg').text('Do you really want to clear all history?') - $('.ui-delete-modal-item').html('There is no turning back.') + $('.ui-delete-history-modal-msg').text('Do you really want to clear all history?') + $('.ui-delete-history-modal-item').html('There is no turning back.') clearHistory = true deleteId = null }) @@ -371,6 +371,10 @@ $('.ui-refresh-history').click(() => { }) }) +$('.ui-delete-user-modal-cancel').click(() => { + $('.ui-delete-user').parent().removeClass('active') +}) + $('.ui-logout').click(() => { clearLoginState() location.href = `${serverurl}/logout` diff --git a/public/js/extra.js b/public/js/extra.js index dbb0f45..d6bbb0c 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -105,9 +105,9 @@ function getTitle (view) { export function renderTitle (view) { let title = getTitle(view) if (title) { - title += ' - HackMD' + title += ' - CodiMD' } else { - title = 'HackMD - Collaborative markdown notes' + title = 'CodiMD - Collaborative markdown notes' } return title } diff --git a/public/js/history.js b/public/js/history.js index 7132281..b4c26b4 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -1,5 +1,5 @@ /* eslint-env browser, jquery */ -/* global serverurl, Cookies, moment */ +/* global serverurl, moment */ import store from 'store' import S from 'string' @@ -64,13 +64,7 @@ export function saveHistory (notehistory) { } function saveHistoryToStorage (notehistory) { - if (store.enabled) { store.set('notehistory', JSON.stringify(notehistory)) } else { saveHistoryToCookie(notehistory) } -} - -function saveHistoryToCookie (notehistory) { - Cookies.set('notehistory', notehistory, { - expires: 365 - }) + store.set('notehistory', JSON.stringify(notehistory)) } function saveHistoryToServer (notehistory) { @@ -150,35 +144,17 @@ export function writeHistory (title, tags) { ) } -function writeHistoryToCookie (title, tags) { - var notehistory - try { - notehistory = Cookies.getJSON('notehistory') - } catch (err) { +function writeHistoryToStorage (title, tags) { + let data = store.get('notehistory') + let notehistory + if (data && typeof data === 'string') { + notehistory = JSON.parse(data) + } else { notehistory = [] } - if (!notehistory) { notehistory = [] } + const newnotehistory = generateHistory(title, tags, notehistory) - saveHistoryToCookie(newnotehistory) -} - -function writeHistoryToStorage (title, tags) { - if (store.enabled) { - let data = store.get('notehistory') - var notehistory - if (data) { - if (typeof data === 'string') { data = JSON.parse(data) } - notehistory = data - } else { - notehistory = [] - } - if (!notehistory) { notehistory = [] } - - const newnotehistory = generateHistory(title, tags, notehistory) - saveHistoryToStorage(newnotehistory) - } else { - writeHistoryToCookie(title, tags) - } + saveHistoryToStorage(newnotehistory) } if (!Array.isArray) { @@ -236,20 +212,13 @@ function getServerHistory (callback) { }) } -function getCookieHistory (callback) { - callback(Cookies.getJSON('notehistory')) -} - export function getStorageHistory (callback) { - if (store.enabled) { - let data = store.get('notehistory') - if (data) { - if (typeof data === 'string') { data = JSON.parse(data) } - callback(data) - } else { getCookieHistory(callback) } - } else { - getCookieHistory(callback) + let data = store.get('notehistory') + if (data) { + if (typeof data === 'string') { data = JSON.parse(data) } + callback(data) } + callback([]) } export function parseHistory (list, callback) { @@ -275,21 +244,13 @@ export function parseServerToHistory (list, callback) { }) } -function parseCookieToHistory (list, callback) { - const notehistory = Cookies.getJSON('notehistory') - parseToHistory(list, notehistory, callback) -} - export function parseStorageToHistory (list, callback) { - if (store.enabled) { - let data = store.get('notehistory') - if (data) { - if (typeof data === 'string') { data = JSON.parse(data) } - parseToHistory(list, data, callback) - } else { parseCookieToHistory(list, callback) } - } else { - parseCookieToHistory(list, callback) + let data = store.get('notehistory') + if (data) { + if (typeof data === 'string') { data = JSON.parse(data) } + parseToHistory(list, data, callback) } + parseToHistory(list, [], callback) } function parseToHistory (list, notehistory, callback) { diff --git a/public/js/index.js b/public/js/index.js index c6a4f77..6e13fe9 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -566,7 +566,10 @@ var previousFocusOnEditor = null function checkEditorStyle () { var desireHeight = editorInstance.statusBar ? (ui.area.edit.height() - editorInstance.statusBar.outerHeight()) : ui.area.edit.height() - // set editor height and min height based on scrollbar style and mode + if (editorInstance.toolBar) { + desireHeight = desireHeight - editorInstance.toolBar.outerHeight() + } + // set editor height and min height based on scrollbar style and mode var scrollbarStyle = editor.getOption('scrollbarStyle') if (scrollbarStyle === 'overlay' || appState.currentMode === modeType.both) { ui.area.codemirrorScroll.css('height', desireHeight + 'px') @@ -804,6 +807,10 @@ function changeMode (type) { editorInstance.addStatusBar() editorInstance.updateStatusBar() } + // add and update tool bar + if (!editorInstance.toolBar) { + editorInstance.addToolBar() + } // work around foldGutter might not init properly editor.setOption('foldGutter', false) editor.setOption('foldGutter', true) diff --git a/public/js/lib/config/index.js b/public/js/lib/config/index.js index 4758ffe..6133e2c 100644 --- a/public/js/lib/config/index.js +++ b/public/js/lib/config/index.js @@ -7,7 +7,7 @@ export const debug = window.debug || false export const port = window.location.port export const serverurl = `${window.location.protocol}//${domain || window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}` window.serverurl = serverurl -export const noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1] +export const noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]) export const noteurl = `${serverurl}/${noteid}` export const version = window.version diff --git a/public/js/lib/editor/index.js b/public/js/lib/editor/index.js index bc228b7..0537e92 100644 --- a/public/js/lib/editor/index.js +++ b/public/js/lib/editor/index.js @@ -1,6 +1,7 @@ import * as utils from './utils' import config from './config' import statusBarTemplate from './statusbar.html' +import toolBarTemplate from './toolbar.html' /* config section */ const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault @@ -136,6 +137,88 @@ export default class Editor { }) } + addToolBar () { + this.toolBar = $(toolBarTemplate) + this.toolbarPanel = this.editor.addPanel(this.toolBar[0], { + position: 'top' + }) + + var makeBold = $('#makeBold') + var makeItalic = $('#makeItalic') + var makeStrike = $('#makeStrike') + var makeHeader = $('#makeHeader') + var makeCode = $('#makeCode') + var makeQuote = $('#makeQuote') + var makeGenericList = $('#makeGenericList') + var makeOrderedList = $('#makeOrderedList') + var makeCheckList = $('#makeCheckList') + var makeLink = $('#makeLink') + var makeImage = $('#makeImage') + var makeTable = $('#makeTable') + var makeLine = $('#makeLine') + var makeComment = $('#makeComment') + + makeBold.click(() => { + utils.wrapTextWith(this.editor, this.editor, '**') + this.editor.focus() + }) + + makeItalic.click(() => { + utils.wrapTextWith(this.editor, this.editor, '*') + this.editor.focus() + }) + + makeStrike.click(() => { + utils.wrapTextWith(this.editor, this.editor, '~~') + this.editor.focus() + }) + + makeHeader.click(() => { + utils.insertHeader(this.editor) + }) + + makeCode.click(() => { + utils.wrapTextWith(this.editor, this.editor, '```') + this.editor.focus() + }) + + makeQuote.click(() => { + utils.insertOnStartOfLines(this.editor, '> ') + }) + + makeGenericList.click(() => { + utils.insertOnStartOfLines(this.editor, '* ') + }) + + makeOrderedList.click(() => { + utils.insertOnStartOfLines(this.editor, '1. ') + }) + + makeCheckList.click(() => { + utils.insertOnStartOfLines(this.editor, '- [ ] ') + }) + + makeLink.click(() => { + utils.insertLink(this.editor, false) + }) + + makeImage.click(() => { + utils.insertLink(this.editor, true) + }) + + makeTable.click(() => { + utils.insertText(this.editor, '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n') + }) + + makeLine.click(() => { + utils.insertText(this.editor, '\n----\n') + }) + + makeComment.click(() => { + utils.insertText(this.editor, '> []') + }) + } + addStatusBar () { this.statusBar = $(statusBarTemplate) this.statusCursor = this.statusBar.find('.status-cursor > .status-line-column') diff --git a/public/js/lib/editor/toolbar.html b/public/js/lib/editor/toolbar.html new file mode 100644 index 0000000..a2ac476 --- /dev/null +++ b/public/js/lib/editor/toolbar.html @@ -0,0 +1,48 @@ + diff --git a/public/js/lib/editor/utils.js b/public/js/lib/editor/utils.js index 3702a16..36e5c12 100644 --- a/public/js/lib/editor/utils.js +++ b/public/js/lib/editor/utils.js @@ -3,39 +3,39 @@ export function wrapTextWith (editor, cm, symbol) { if (!cm.getSelection()) { return CodeMirror.Pass } else { - var ranges = cm.listSelections() - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i] + let ranges = cm.listSelections() + for (let i = 0; i < ranges.length; i++) { + let range = ranges[i] if (!range.empty()) { const from = range.from() const to = range.to() if (symbol !== 'Backspace') { - cm.replaceRange(symbol, to, to, '+input') - cm.replaceRange(symbol, from, from, '+input') - // workaround selection range not correct after add symbol - var _ranges = cm.listSelections() - var anchorIndex = editor.indexFromPos(_ranges[i].anchor) - var headIndex = editor.indexFromPos(_ranges[i].head) + let selection = cm.getRange(from, to) + let anchorIndex = editor.indexFromPos(ranges[i].anchor) + let headIndex = editor.indexFromPos(ranges[i].head) + cm.replaceRange(symbol + selection + symbol, from, to, '+input') if (anchorIndex > headIndex) { - _ranges[i].anchor.ch-- + ranges[i].anchor.ch += symbol.length + ranges[i].head.ch += symbol.length } else { - _ranges[i].head.ch-- + ranges[i].head.ch += symbol.length + ranges[i].anchor.ch += symbol.length } - cm.setSelections(_ranges) + cm.setSelections(ranges) } else { - var preEndPos = { + let preEndPos = { line: to.line, - ch: to.ch + 1 + ch: to.ch + symbol.length } - var preText = cm.getRange(to, preEndPos) - var preIndex = wrapSymbols.indexOf(preText) - var postEndPos = { + let preText = cm.getRange(to, preEndPos) + let preIndex = wrapSymbols.indexOf(preText) + let postEndPos = { line: from.line, - ch: from.ch - 1 + ch: from.ch - symbol.length } - var postText = cm.getRange(postEndPos, from) - var postIndex = wrapSymbols.indexOf(postText) + let postText = cm.getRange(postEndPos, from) + let postIndex = wrapSymbols.indexOf(postText) // check if surround symbol are list in array and matched if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) { cm.replaceRange('', to, preEndPos, '+delete') @@ -46,3 +46,76 @@ export function wrapTextWith (editor, cm, symbol) { } } } + +export function insertText (cm, text, cursorEnd = 0) { + let cursor = cm.getCursor() + cm.replaceSelection(text, cursor, cursor) + cm.focus() + cm.setCursor({line: cursor.line, ch: cursor.ch + cursorEnd}) +} + +export function insertLink (cm, isImage) { + let cursor = cm.getCursor() + let ranges = cm.listSelections() + const linkEnd = '](https://)' + const symbol = (isImage) ? '![' : '[' + + for (let i = 0; i < ranges.length; i++) { + let range = ranges[i] + if (!range.empty()) { + const from = range.from() + const to = range.to() + let anchorIndex = editor.indexFromPos(ranges[i].anchor) + let headIndex = editor.indexFromPos(ranges[i].head) + let selection = cm.getRange(from, to) + selection = symbol + selection + linkEnd + cm.replaceRange(selection, from, to) + if (anchorIndex > headIndex) { + ranges[i].anchor.ch += symbol.length + ranges[i].head.ch += symbol.length + } else { + ranges[i].head.ch += symbol.length + ranges[i].anchor.ch += symbol.length + } + cm.setSelections(ranges) + } else { + cm.replaceRange(symbol + linkEnd, cursor, cursor) + cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length}) + } + } + cm.focus() +} + +export function insertHeader (cm) { + let cursor = cm.getCursor() + let startOfLine = {line: cursor.line, ch: 0} + let startOfLineText = cm.getRange(startOfLine, {line: cursor.line, ch: 1}) + // See if it is already a header + if (startOfLineText === '#') { + cm.replaceRange('#', startOfLine, startOfLine) + } else { + cm.replaceRange('# ', startOfLine, startOfLine) + } + cm.focus() +} + +export function insertOnStartOfLines (cm, symbol) { + let cursor = cm.getCursor() + let ranges = cm.listSelections() + + for (let i = 0; i < ranges.length; i++) { + let range = ranges[i] + if (!range.empty()) { + const from = range.from() + const to = range.to() + let selection = cm.getRange({line: from.line, ch: 0}, to) + selection = selection.replace(/\n/g, '\n' + symbol) + selection = symbol + selection + cm.replaceRange(selection, from, to) + } else { + cm.replaceRange(symbol, {line: cursor.line, ch: 0}, {line: cursor.line, ch: 0}) + } + } + cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length}) + cm.focus() +} diff --git a/public/js/slide.js b/public/js/slide.js index 59a352e..1eb8dfd 100644 --- a/public/js/slide.js +++ b/public/js/slide.js @@ -14,6 +14,7 @@ window.lastchangetime = window.lastchangeui.time.attr('data-updatetime') updateLastChange() const url = window.location.pathname $('.ui-edit').attr('href', `${url}/edit`) +$('.ui-print').attr('href', `${url}?print-pdf`) $(document).ready(() => { // tooltip diff --git a/public/vendor/md-toc.js b/public/vendor/md-toc.js index 3457d46..876978f 100644 --- a/public/vendor/md-toc.js +++ b/public/vendor/md-toc.js @@ -28,7 +28,7 @@ Toc.prototype._collectTitleElements = function () { this._elTitlesNames = [] this.elTitleElements = [] - for (var i = 1; i < 7; i++) { + for (var i = 1; i < 6; i++) { if (this.el.getElementsByTagName('h' + i).length) { this._elTitlesNames.push('h' + i) } @@ -44,59 +44,79 @@ } } - Toc.prototype._createTocContent = function () { - this._elTitleElementsLen = this.elTitleElements.length - if (!this._elTitleElementsLen) return - this.tocContent = '' - this._tempLists = [] + Toc.prototype._createTocContent = function recursiveToc(level = 0, titleElements = [], titleNames = [], ulClass = undefined) { + // Inititalize our elements from the toc object + // which is only available on level 0 + if (level === 0) { + titleElements = this.elTitleElements + titleNames = this._elTitlesNames + ulClass = this.ulClass + } + // No need to do anything for an empty ToC + if (!titleElements.length) return - for (var i = 0; i < this._elTitleElementsLen; i++) { - var j = i + 1 - this._elTitleElement = this.elTitleElements[i] - this._elTitleElementName = this._elTitleElement.tagName - this._elTitleElementTitle = this._elTitleElement.textContent.replace(/"/g, '"') - this._elTitleElementText = (typeof this.process === 'function' ? this.process(this._elTitleElement) : this._elTitleElement.innerHTML).replace(/<(?:.|\n)*?>/gm, '') - var id = this._elTitleElement.getAttribute('id') - if (!id) { - this._elTitleElement.setAttribute('id', 'tip' + i) - id = '#tip' + i - } else { - id = '#' + id - } + var content = '' + this._elTitleElementText + '' + for (var element; element = titleElements.shift();) { + var elementTag = element.tagName.toLowerCase() - if (j !== this._elTitleElementsLen) { - this._elNextTitleElementName = this.elTitleElements[j].tagName - if (this._elTitleElementName !== this._elNextTitleElementName) { - var checkColse = false - var y = 1 - for (var t = this._tempLists.length - 1; t >= 0; t--) { - if (this._tempLists[t].tagName === this._elNextTitleElementName) { - checkColse = true - break - } - y++ - } - if (checkColse) { - this.tocContent += new Array(y + 1).join('') - this._tempLists.length = this._tempLists.length - y - } else { - this._tempLists.push(this._elTitleElement) - if (this.ulClass) { this.tocContent += '
      ' } else { this.tocContent += '
        ' } - } + // We only care about tags on our level to add them as list item + if (elementTag == iterTag) { + // Let's do some cleaning + var elementTitle = element.textContent.replace(/"/g, '"') + var elementText = (typeof this.process === 'function' ? this.process(element) : element.innerHTML).replace(/<(?:.|\n)*?>/gm, '') + var id = element.getAttribute('id') + if (!id) { + element.setAttribute('id', 'tip' + i) + id = '#tip' + i } else { - this.tocContent += '' + id = '#' + id } + if (openTag) { + content += '\n' + openTag = false + } + content += '
      • ' + elementText + '' + // Reset recursion. We need it for the next subsections + recurse = false + openTag = true + // Check if the current element has a lower level than ours, if so, we have to go down the rabbithole! + } else if (!recurse && titleNames.indexOf(elementTag.toLowerCase()) > level) { + recurse = true + if (!openTag) { + content += '
      • ' + openTag = true + } + // This element is for the lower lever, we have to re-add it before we send the list down there. + titleElements.unshift(element) + // Let's call ourself and get to the next level + content += recursiveToc(level + 1, titleElements, titleNames, ulClass) } else { - if (this._tempLists.length) { - this.tocContent += new Array(this._tempLists.length + 1).join('
      ') - } else { - this.tocContent += '' - } + // When we end up here, met a higher level element + // This is not our business so back into the list with the element and let's end this loop + titleElements.unshift(element) + break } } - if (this.ulClass) { this.tocContent = '
        ' + this.tocContent + '
      ' } else { this.tocContent = '
        ' + this.tocContent + '
      ' } + + if (openTag) { + content += '\n' + } + content += '
    \n' + + // Set ToC content of the level 0 everything else pass things to the upper level! + if (level === 0) { + this.tocContent = content + } else { + return content + } } Toc.prototype._showToc = function () { diff --git a/public/views/codimd.ejs b/public/views/codimd.ejs new file mode 100644 index 0000000..7818346 --- /dev/null +++ b/public/views/codimd.ejs @@ -0,0 +1,15 @@ + + + + + <%- include codimd/head %> + + + + <%- include codimd/header %> + <%- include codimd/body %> + <%- include codimd/footer %> + <%- include codimd/foot %> + + + diff --git a/public/views/hackmd/body.ejs b/public/views/codimd/body.ejs similarity index 99% rename from public/views/hackmd/body.ejs rename to public/views/codimd/body.ejs index 4960437..b5932a6 100644 --- a/public/views/hackmd/body.ejs +++ b/public/views/codimd/body.ejs @@ -233,7 +233,7 @@ -