diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 685b64a84..000000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -old_src/lib/ot -old_src/lib/migrations -public/vendor -public/build -node_modules -build diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8de6e3bed..1736721bb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve HedgeDoc. title: '' -labels: 'bug' +labels: 'type: bug' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.md b/.github/ISSUE_TEMPLATE/enhancement_request.md index 3d7af9274..dee13b03c 100644 --- a/.github/ISSUE_TEMPLATE/enhancement_request.md +++ b/.github/ISSUE_TEMPLATE/enhancement_request.md @@ -2,7 +2,7 @@ name: Enhancement request about: Suggest an enhancement of an existing feature. title: '' -labels: 'enhancement' +labels: 'type: enhancement' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 98ef5148f..8a861a4a8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest a new feature for this project, which isn't existing yet. title: '' -labels: 'feature request' +labels: 'type: feature' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/question---other.md b/.github/ISSUE_TEMPLATE/question---other.md index 757ba1824..36fd33363 100644 --- a/.github/ISSUE_TEMPLATE/question---other.md +++ b/.github/ISSUE_TEMPLATE/question---other.md @@ -2,7 +2,7 @@ name: Question / Other about: Questions about the project, features, or organziational issues title: '' -labels: question +labels: 'type: question' assignees: '' --- diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fbc61e54b..bc03d58c8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,12 +6,13 @@ This PR fixes/adds/improves/... ### Steps - + -- [ ] added implementation -- [ ] added / updated tests -- [ ] added / updated documentation -- [ ] extended changelog +- [ ] Added implementation +- [ ] Added / updated tests +- [ ] Added / updated documentation +- [ ] I read the [contribution documentation](https://github.com/hedgedoc/hedgedoc/blob/develop/CONTRIBUTING.md) and + signed-off my commits to accept the DCO. ### Related Issue(s) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f6a58300d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: node_js -dist: xenial -cache: yarn - -stages: - - Static Tests - - test - -node_js: - - 10 - - 12 - - 14 -env: - - TEST_SUITE=test - - TEST_SUITE=test:e2e -script: "yarn run $TEST_SUITE" - -jobs: - include: - - stage: Static Tests - name: eslint - script: - - yarn run lint - - name: ShellCheck - script: - - shellcheck bin/heroku bin/setup - language: generic diff --git a/README.md b/README.md index 0ebceaab3..e9017c883 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ which you are very welcome to join. Licensed under AGPLv3. For our list of contributors, see [AUTHORS](AUTHORS). +The license does not include the HedgeDoc logo, whose terms of usage can be found in +the [github repository](https://github.com/hedgedoc/hedgedoc-logo). + [matrix.org-image]: https://img.shields.io/matrix/hedgedoc:matrix.org?logo=matrix&server_fqdn=matrix.org [matrix.org-url]: https://chat.hedgedoc.org @@ -51,15 +54,9 @@ Licensed under AGPLv3. For our list of contributors, see [AUTHORS](AUTHORS). [poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg [poeditor-url]: https://poeditor.com/join/project/1OpGjF2Jir - [hedgedoc-demo]: https://demo.hedgedoc.org - [hedgedoc-demo-features]: https://demo.hedgedoc.org/features - [hedgedoc-community]: https://community.hedgedoc.org - [hedgedoc-community-calls]: https://community.hedgedoc.org/t/codimd-community-call/19 - [social-mastodon]: https://social.hedgedoc.org/mastodon - [social-mastodon-image]: https://img.shields.io/mastodon/follow/49593?domain=https%3A%2F%2Fsocial.snopyta.org&style=social diff --git a/app.json b/app.json deleted file mode 100644 index 63d6b323b..000000000 --- a/app.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "name": "CodiMD", - "description": "Realtime collaborative markdown notes on all platforms", - "keywords": [ - "Collaborative", - "Markdown", - "Notes" - ], - "website": "https://codimd.org", - "repository": "https://github.com/codimd/server", - "logo": "https://github.com/codimd/server/raw/master/public/codimd-icon-1024.png", - "success_url": "/", - "env": { - "NPM_CONFIG_PRODUCTION": { - "description": "Let npm also install development build tool", - "value": "false" - }, - "DB_TYPE": { - "description": "Specify database type. See sequelize available databases. Default using postgres", - "value": "postgres" - }, - "CMD_SESSION_SECRET": { - "description": "Secret used to secure session cookies.", - "required": false - }, - "CMD_HSTS_ENABLE": { - "description": "whether to also use HSTS if HTTPS is enabled", - "required": false - }, - "CMD_HSTS_MAX_AGE": { - "description": "max duration, in seconds, to tell clients to keep HSTS status", - "required": false - }, - "CMD_HSTS_INCLUDE_SUBDOMAINS": { - "description": "whether to tell clients to also regard subdomains as HSTS hosts", - "required": false - }, - "CMD_HSTS_PRELOAD": { - "description": "whether to allow at all adding of the site to HSTS preloads (e.g. in browsers)", - "required": false - }, - "CMD_DOMAIN": { - "description": "domain name", - "required": false - }, - "CMD_URL_PATH": { - "description": "sub url path, like `www.example.com/`", - "required": false - }, - "CMD_ALLOW_ORIGIN": { - "description": "domain name whitelist (use comma to separate)", - "required": false, - "value": "localhost" - }, - "CMD_PROTOCOL_USESSL": { - "description": "set to use ssl protocol for resources path (only applied when domain is set)", - "required": false - }, - "CMD_URL_ADDPORT": { - "description": "set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set)", - "required": false - }, - "CMD_FACEBOOK_CLIENTID": { - "description": "Facebook API client id", - "required": false - }, - "CMD_FACEBOOK_CLIENTSECRET": { - "description": "Facebook API client secret", - "required": false - }, - "CMD_TWITTER_CONSUMERKEY": { - "description": "Twitter API consumer key", - "required": false - }, - "CMD_TWITTER_CONSUMERSECRET": { - "description": "Twitter API consumer secret", - "required": false - }, - "CMD_GITHUB_CLIENTID": { - "description": "GitHub API client id", - "required": false - }, - "CMD_GITHUB_CLIENTSECRET": { - "description": "GitHub API client secret", - "required": false - }, - "CMD_GITLAB_BASEURL": { - "description": "GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional)", - "required": false - }, - "CMD_GITLAB_CLIENTID": { - "description": "GitLab API client id", - "required": false - }, - "CMD_GITLAB_CLIENTSECRET": { - "description": "GitLab API client secret", - "required": false - }, - "CMD_GITLAB_SCOPE": { - "description": "GitLab API client scope (optional)", - "required": false - }, - "CMD_DROPBOX_CLIENTID": { - "description": "Dropbox API client id", - "required": false - }, - "CMD_DROPBOX_CLIENTSECRET": { - "description": "Dropbox API client secret", - "required": false - }, - "CMD_DROPBOX_APP_KEY": { - "description": "Dropbox app key (for import/export)", - "required": false - }, - "CMD_GOOGLE_CLIENTID": { - "description": "Google API client id", - "required": false - }, - "CMD_GOOGLE_CLIENTSECRET": { - "description": "Google API client secret", - "required": false - }, - "CMD_GOOGLE_HOSTEDDOMAIN": { - "description": "Google API hosted domain (Provided only if the user belongs to a hosted domain)", - "required": false - }, - "CMD_IMGUR_CLIENTID": { - "description": "Imgur API client id", - "required": false - } - }, - "addons": [ - "heroku-postgresql" - ] -} diff --git a/config.json.example b/config.json.example deleted file mode 100644 index b1766c5cc..000000000 --- a/config.json.example +++ /dev/null @@ -1,126 +0,0 @@ -{ - "test": { - "db": { - "dialect": "sqlite", - "storage": ":memory:" - }, - "linkifyHeaderStyle": "gfm" - }, - "development": { - "loglevel": "debug", - "hsts": { - "enable": false - }, - "db": { - "dialect": "sqlite", - "storage": "./db.codimd.sqlite" - }, - "linkifyHeaderStyle": "gfm" - }, - "production": { - "domain": "localhost", - "loglevel": "info", - "hsts": { - "enable": true, - "maxAgeSeconds": 31536000, - "includeSubdomains": true, - "preload": true - }, - "csp": { - "enable": true, - "directives": { - }, - "upgradeInsecureRequests": "auto", - "addDefaults": true, - "addDisqus": true, - "addGoogleAnalytics": true - }, - "db": { - "username": "", - "password": "", - "database": "codimd", - "host": "localhost", - "port": "5432", - "dialect": "postgres" - }, - "facebook": { - "clientID": "change this", - "clientSecret": "change this" - }, - "twitter": { - "consumerKey": "change this", - "consumerSecret": "change this" - }, - "github": { - "clientID": "change this", - "clientSecret": "change this" - }, - "gitlab": { - "baseURL": "change this", - "clientID": "change this", - "clientSecret": "change this", - "scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')", - "version": "use 'v4' if gitlab version > 11, 'v3' otherwise. Default to 'v4'" - }, - "dropbox": { - "clientID": "change this", - "clientSecret": "change this", - "appKey": "change this" - }, - "google": { - "clientID": "change this", - "clientSecret": "change this", - "apiKey": "change this" - }, - "ldap": { - "url": "ldap://change_this", - "bindDn": null, - "bindCredentials": null, - "searchBase": "change this", - "searchFilter": "change this", - "searchAttributes": ["change this"], - "usernameField": "change this e.g. cn", - "useridField": "change this e.g. uid", - "tlsOptions": { - "changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback" - } - }, - "saml": { - "idpSsoUrl": "change: authentication endpoint of IdP", - "idpCert": "change: certificate file path of IdP in PEM format", - "issuer": "change or delete: identity of the service provider (default: config.serverURL)", - "identifierFormat": "change or delete: name identifier format (default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')", - "disableRequestedAuthnContext": "change or delete: true to allow any authentication method, false restricts to password authentication method (default: false)", - "groupAttribute": "change or delete: attribute name for group list (ex: memberOf)", - "requiredGroups": [ "change or delete: group names that allowed" ], - "externalGroups": [ "change or delete: group names that not allowed" ], - "attribute": { - "id": "change or delete this: attribute map for `id` (default: NameID)", - "username": "change or delete this: attribute map for `username` (default: NameID)", - "email": "change or delete this: attribute map for `email` (default: NameID)" - } - }, - "imgur": { - "clientID": "change this" - }, - "minio": { - "accessKey": "change this", - "secretKey": "change this", - "endPoint": "change this", - "secure": true, - "port": 9000 - }, - "s3": { - "accessKeyId": "change this", - "secretAccessKey": "change this", - "region": "change this" - }, - "s3bucket": "change this", - "azure": - { - "connectionString": "change this", - "container": "change this" - }, - "linkifyHeaderStyle": "gfm" - } -} diff --git a/docs/configuration-config-file.md b/docs/configuration-config-file.md deleted file mode 100644 index db00e3a82..000000000 --- a/docs/configuration-config-file.md +++ /dev/null @@ -1,189 +0,0 @@ -Configuration Using Config file -=== - -You can choose to configure CodiMD with either a config file or with -[environment variables](configuration-env-vars.md). The config file is processed -in [`lib/config/index.js`](../lib/config/index.js) - so this is the first -place to look if anything is missing not obvious from this document. The -default values are defined in [`lib/config/default.js`](../lib/config/default.js), -in case you wonder if you even need to override it. - -Environment variables take precedence over configurations from the config files. -To get started, it is a good idea to take the `config.json.example` and copy it -to `config.json` before filling in your own details. - - -## Node.JS - -| variables | example values | description | -| --------- | ------ | ----------- | -| `debug` | `true` or `false` | set debug mode, show more logs | - - -## CodiMD basics - -| variables | example values | description | -| --------- | ------ | ----------- | -| `db` | `{ "dialect": "sqlite", "storage": "./db.codimd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) | -| `dbURL` | `mysql://localhost:3306/database` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. | -| `forbiddenNoteIDs` | `['robots.txt']` | disallow creation of notes, even if `allowFreeUrl` is `true` | -| `loglevel` | `info` | Defines what kind of logs are provided to stdout. Available options: `debug`, `verbose`, `info`, `warn`, `error` | -| `imageUploadType` | `imgur`, `s3`, `minio`, `azure`, `lutim` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [MinIO](guides/minio-image-upload.md)| -| `sourceURL` | `https://github.com/codimd/server/tree/` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) | -| `staticCacheTime` | `1 * 24 * 60 * 60 * 1000` | static file cache time | -| `tooBusyLag` | `70` | CPU time for one event loop tick until node throttles connections. (milliseconds) | -| `heartbeatInterval` | `5000` | socket.io heartbeat interval | -| `heartbeatTimeout` | `10000` | socket.io heartbeat timeout | -| `documentMaxLength` | `100000` | note max length | - - -## CodiMD paths stuff - -these are rarely used for various reasons. - -| variables | example values | description | -| --------- | ------ | ----------- | -| `defaultNotePath` | `./public/default.md` | default note file path1, empty notes will be created with this template. | -| `dhParamPath` | `./cert/dhparam.pem` | SSL dhparam path1 (only need when you set `useSSL`) | -| `sslCAPath` | `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain1 (only need when you set `useSSL`) | -| `sslCertPath` | `./cert/codimd_io.crt` | SSL cert path1 (only need when you set `useSSL`) | -| `sslKeyPath` | `./cert/client.key` | SSL key path1 (only need when you set `useSSL`) | -| `tmpPath` | `./tmp/` | temp directory path1 | -| `docsPath` | `./public/docs` | docs directory path1 | -| `viewPath` | `./public/views` | template directory path1 | -| `uploadsPath` | `./public/uploads` | uploads directory1 - needs to be persistent when you use imageUploadType `filesystem` | -| `localesPath` | `./locales` | directory for translations1 | - - -## CodiMD Location - -| variables | example values | description | -| --------- | ------ | ----------- | -| `domain` | `localhost` | domain name | -| `urlPath` | `codimd` | sub URL path, like `www.example.com/` | -| `host` | `localhost` | interface/ip to listen on | -| `port` | `80` | port to listen on | -| `path` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `host` and `port` are ignored) | -| `protocolUseSSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) | -| `useSSL` | `true` or `false` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) | -| `urlAddPort` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | -| `allowOrigin` | `['localhost']` | domain name whitelist | - - -## CSP and HSTS - -| variables | example values | description | -| --------- | ------ | ----------- | -| `hsts` | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | -| `csp` | `{"enable": true, "directives": {"scriptSrc": "trustworthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": true}` | Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. Some defaults are added to the configured values so that the application doesn't break. To disable this behaviour, set `addDefaults` to `false`. Further, if `usecdn` is on, some CDN locations are allowed too. By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set `upgradeInsecureRequests` to either `true` or `false`. | - -## Privacy and External Requests - -| variables | example values | description | -| --------- | ------ | ----------- | -| `allowGravatar` | `true` or `false` | set to `false` to disable [Libravatar](https://www.libravatar.org/) as profile picture source on your instance. Libravatar is a federated open-source alternative to Gravatar. | -| `useCDN` | `true` or `false` | set to use CDN resources or not (default is `false`) | - -## Users and Privileges - -| variables | example values | description | -| --------- | ------ | ----------- | -| `allowAnonymous` | `true` or `false` | Set to allow anonymous usage (default is `true`). | -| `allowAnonymousEdits` | `true` or `false` | If `allowAnonymous` is `true`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`). | -| `allowFreeURL` | `true` or `false` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). | -| `defaultPermission` | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). | -| `sessionName` | `connect.sid` | Cookie session name. | -| `sessionLife` | `14 * 24 * 60 * 60 * 1000` (14 days) | Cookie session life time in milliseconds. | -| `sessionSecret` | `secret` | Cookie session secret. If none is set, one will randomly generated on each startup, meaning all your users will be logged out. | - - -## Login methods - -### Email (local account) - -| variables | example values | description | -| --------- | ------ | ----------- | -| `email` | `true` or `false` | Set to allow email sign-in. The default is `true`. | -| `allowEmailRegister` | `true` or `false` | Set to allow registration of new accounts using an email address. If set to `false`, you can still create accounts using the command line - see `bin/manage_users` for details (In production mode, remember to run it with `NODE_ENV` set as `production` in the enviroment). This setting has no effect if `email` is `false`. The default for `allowEmailRegister` is `true`. | - -### Dropbox Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `dropbox` | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Dropbox developer tools](https://www.dropbox.com/developers/apps) | - -### Facebook Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `facebook` | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the [Facebook app console](https://developers.facebook.com/apps) | - -### GitHub Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `github` | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret obtained by the GitHub developer page. For more details have a look at the [GitHub auth guide](guides/auth/github.md). | - -### GitLab Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `gitlab` | `{baseURL: ..., scope: ..., version: ..., clientID: ..., clientSecret: ...}` | An object containing your GitLab application data. Refer to the [GitLab guide](guides/auth/gitlab-self-hosted.md) for more details! | - -### Google Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `google` | `{clientID: ..., clientSecret: ..., hostedDomain: ...}` | An object containing the client ID and the client secret obtained by the [Google API console](https://console.cloud.google.com/apis) | - -### LDAP Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `ldap` | `{providerName: ..., url: ..., bindDn: ..., bindCredentials: ..., searchBase: ..., searchFilter: ..., searchAttributes: ..., usernameField: ..., useridField: ..., starttls: ..., tlsca: ...}` | An object detailing the LDAP connection. Refer to the [LDAP-AD guide](guides/auth/ldap-AD.md) for more details! | - -### OAuth2 Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `oauth2` | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details!| - -### SAML Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `saml` | `{idpSsoUrl: ..., idpCert: ..., issuer: ..., identifierFormat: ..., disableRequestedAuthnContext: ..., groupAttribute: ..., externalGroups: [], requiredGroups: [], attribute: {id: ..., username: ..., email: ...}}` | An object detailing your SAML provider. Refer to the [OneLogin](guides/auth/saml-onelogin.md) and [SAML](guides/auth/saml.md) guides for more details! | - -### Twitter Login - -| variables | example values | description | -| --------- | ------ | ----------- | -| `twitter` | `{consumerKey: ..., consumerSecret: ...}` | An object containing the consumer key and secret obtained by the [Twitter developer tools](https://developer.twitter.com/apps). For more details have a look at the [Twitter auth guide](guides/auth/twitter.md) | - -## Upload Storage - -Most of these have never been documented for the config.json, feel free to expand these - - -### Amazon S3 - -| variables | example values | description | -| --------- | ------ | ----------- | -| `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](guides/s3-image-upload.md) | -| `s3bucket` | `YOUR_S3_BUCKET_NAME` | bucket name when `imageUploadType` is set to `s3` or `minio` | - -### Azure Blob Storage -### Imgur -### MinIO - -| variables | example values | description | -| --------- | ------ | ----------- | -| `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 check out our [Minio Image Upload Guide](guides/minio-image-upload.md) | - -### Lutim - -| variables | example values | description | -| --------- | ------ | ----------- | -|`lutim`| `{"url": "YOUR_LUTIM_URL"}`| When `imageUploadType` is set to `lutim`, you can setup the lutim url| - -1: relative paths are based on CodiMD's base directory diff --git a/docs/configuration-env-vars.md b/docs/configuration-env-vars.md deleted file mode 100644 index 999e43057..000000000 --- a/docs/configuration-env-vars.md +++ /dev/null @@ -1,248 +0,0 @@ -Configuration Using Environment variables -=== - -You can choose to configure CodiMD with either a -[config file](configuration-config-file.md) or with environment variables. -Environment variables are processed in -[`lib/config/environment.js`](../lib/config/environment.js) - so this is the first -place to look if anything is missing not obvious from this document. The -default values are defined in [`lib/config/default.js`](../lib/config/default.js), -in case you wonder if you even need to override it. - -Environment variables take precedence over configurations from the config files. -They generally start with `CMD_` for our own options, but we also list -node-specific options you can configure this way. - - -## Node.JS - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) | -| `DEBUG` | `true` or `false` | set debug mode; show more logs | - - -## CodiMD basics - -defaultNotePath can't be set from env-vars - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_CONFIG_FILE` | `/path/to/config.json` | optional override for the path to CodiMD's config file | -| `CMD_DB_URL` | `mysql://localhost:3306/database` | Set the db in URL style. If set, then the relevant `db` config entries will be overridden. | -| `CMD_LOGLEVEL` | `info`, `debug` ... | Defines what kind of logs are provided to stdout. | -| `CMD_FORBIDDEN_NOTE_IDS` | `'robots.txt'` | disallow creation of notes, even if `CMD_ALLOW_FREEURL` is `true` | -| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio`, `lutim` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. | -| `CMD_SOURCE_URL` | `https://github.com/codimd/server/tree/` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) | -| `CMD_TOOBUSY_LAG` | `70` | CPU time for one event loop tick until node throttles connections. (milliseconds) | - - -## CodiMD Location - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_DOMAIN` | `codimd.org` | domain name | -| `CMD_URL_PATH` | `codimd` | If CodiMD is run from a subdirectory like `www.example.com/` | -| `CMD_HOST` | `localhost` | interface/ip to listen on | -| `CMD_PORT` | `80` | port to listen on | -| `CMD_PATH` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `CMD_HOST` and `CMD_PORT` are ignored) | -| `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_ALLOW_ORIGIN` | `localhost, codimd.org` | domain name whitelist (use comma to separate) | - - -## CSP and HSTS - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_CSP_ENABLE` | `true` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | -| `CMD_CSP_REPORTURI` | `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | -| `CMD_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) | - - -## Privacy and External Requests - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_ALLOW_GRAVATAR` | `true` or `false` | set to `false` to disable [Libravatar](https://www.libravatar.org/) as profile picture source on your instance. Libravatar is a federated open-source alternative to Gravatar. | -| `CMD_USECDN` | `true` or `false` | set to use CDN resources or not| - - -## Users and Privileges - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_ALLOW_ANONYMOUS` | `true` or `false` | Set to allow anonymous usage (default is `true`). | -| `CMD_ALLOW_ANONYMOUS_EDITS` | `true` or `false` | If `allowAnonymous` is `false`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `true`). | -| `CMD_ALLOW_FREEURL` | `true` or `false` | Set to allow new note creation by accessing a nonexistent note URL. This is the behavior familiar from [Etherpad](https://github.com/ether/etherpad-lite). | -| `CMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied on signed-in users). | -| `CMD_SESSION_LIFE` | `1209600000` (14 days) | Cookie session life time in milliseconds. | -| `CMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If none is set, one will randomly generated on each startup, meaning all your users will be logged out. | - - -## Login methods - -### Email (local account) - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_EMAIL` | `true` or `false` | Set to allow email sign-in. The default is `true`. | -| `CMD_ALLOW_EMAIL_REGISTER` | `true` or `false` | Set to allow registration of new accounts using an email address. If set to `false`, you can still create accounts using the command line - see `bin/manage_users` for details (In production mode, remember to run it with `NODE_ENV` set as `production` in the enviroment). This setting has no effect if `CMD_EMAIL` is `false`. The default for `CMD_ALLOW_EMAIL_REGISTER` is `true`. | - - -### Dropbox Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_DROPBOX_CLIENTID` | no example | Dropbox API client id | -| `CMD_DROPBOX_CLIENTSECRET` | no example | Dropbox API client secret | - - -### Facebook Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_FACEBOOK_CLIENTID` | no example | Facebook API client id | -| `CMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret | - - -### GitHub Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_GITHUB_CLIENTID` | no example | GitHub API client id | -| `CMD_GITHUB_CLIENTSECRET` | no example | GitHub API client secret | - - -### GitLab Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_GITLAB_SCOPE` | `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) | -| `CMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) | -| `CMD_GITLAB_CLIENTID` | no example | GitLab API client id | -| `CMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret | -| `CMD_GITLAB_VERSION` | no example | GitLab API version (v3 or v4) | - - -### Google Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_GOOGLE_CLIENTID` | no example | Google API client id | -| `CMD_GOOGLE_CLIENTSECRET` | no example | Google API client secret | -| `CMD_GOOGLE_HOSTEDDOMAIN` | `example.com` | Provided only if the user belongs to a hosted domain. default is `undefined` | - - -### LDAP Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `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 | - - -### OAuth2 Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_OAUTH2_USER_PROFILE_URL` | `https://example.com` | Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. | -| `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | `name` | where to find the username in the JSON from the user profile URL. (no default value)| -| `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | `display-name` | where to find the display-name in the JSON from the user profile URL. (no default value) | -| `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | `email` | where to find the email address in the JSON from the user profile URL. (no default value) | -| `CMD_OAUTH2_TOKEN_URL` | `https://example.com` | sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) | -| `CMD_OAUTH2_AUTHORIZATION_URL` | `https://example.com` | authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) | -| `CMD_OAUTH2_CLIENT_ID` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) | -| `CMD_OAUTH2_CLIENT_SECRET` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) | -| `CMD_OAUTH2_SCOPE` | `openid email profile` | The requested OAuth2/OIDC scopes, which are privileges that CodiMD can exercise on behalf of the user. Default is `openid email profile`, in order to retrieve user email/profile information via the user profile URL. | -| `CMD_OAUTH2_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the oAuth2 provider | - - -### SAML Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_SAML_IDPSSOURL` | `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](guides/auth/saml-onelogin.md). | -| `CMD_SAML_IDPCERT` | `/path/to/cert.pem` | certificate file path of IdP in PEM format | -| `CMD_SAML_ISSUER` | no example | Issuer to supply to identity provider (optional, default: `serverURL` config)" | -| `CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT` | `true` or `false` | true to allow any authentication method, false restricts to password authentication (PasswordProtectedTransport) method (default: false) | -| `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` | `codimd-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) | - - -### Twitter Login - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key | -| `CMD_TWITTER_CONSUMERSECRET` | no example | Twitter API consumer secret | - - -## Upload Storage - -These are only relevant when they are also configured in sync with their -`CMD_IMAGE_UPLOAD_TYPE`. Also keep in mind, that `filesystem` is available, so -you don't have to use either of these. - - -### Amazon S3 - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `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 | - - -### Azure Blob Storage - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `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) | - - -### imgur - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_IMGUR_CLIENTID` | no example | Imgur API client id | - - -### Minio - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `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 | - - -### Lutim - -| variable | example value | description | -| -------- | ------------- | ----------- | -| `CMD_LUTIM_URL` | `https://framapic.org/` | When `CMD_IMAGE_UPLOAD_TYPE` is set to `lutim`, you can setup the lutim url | - -**Note:** *Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue to work.* - -**Note:** *relative paths are based on CodiMD's base directory* diff --git a/docs/content/configuration.md b/docs/content/configuration.md new file mode 100644 index 000000000..a73e5bbbc --- /dev/null +++ b/docs/content/configuration.md @@ -0,0 +1,445 @@ +# Configuration + +You can choose to configure HedgeDoc with either a config file or with environment variables. + +Environment variables take precedence over configurations from the config files. They generally start with `CMD_` for +our own options, but we also list node-specific options you can configure this way. + +- Environment variables are processed + in [`lib/config/environment.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/environment.js) - so this + is the first place to look if anything is missing not obvious from this document. The default values are defined + in [`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you + wonder if you even need to override it. + +- The config file is processed + in [`lib/config/index.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/index.js) - so this is the + first place to look if anything is missing not obvious from this document. The default values are defined + in [`lib/config/default.js`](https://github.com/hedgedoc/hedgedoc/tree/master/lib/config/default.js), in case you + wonder if you even need to override it. To get started, it is a good idea to take the `config.json.example` and copy + it to `config.json` before filling in your own details. + +**Note:** *Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue +to work.* + +## Node.JS + +| config file | environment | ** +default** and example value | description | | ----------- | ----------- | ----------------------------- | +-------------------------------------------------------------------------------- | | | `NODE_ENV` | `production` +or `development` | set current environment (will apply corresponding settings in the `config.json`) | | `debug` +| `DEBUG` | `true` or `false` | set debug mode, show more logs | + +## HedgeDoc basics + +| config file | environment | ** +default** and example value | description | | ------------------- | ------------------------ | +--------------------------------------------------------------------------------------------------------------------------------------------------- +| +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | | `CMD_CONFIG_FILE` | **no default**, `/path/to/config.json` +| optional override for the path to HedgeDoc's config file | | `db` | | **`undefined`** +, `{ "dialect": "sqlite", "storage": "./db.hedgedoc.sqlite" }` +| set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) +| | `dbURL` | `CMD_DB_URL` | **`undefined`** +, `postgres://username:password@localhost:5432/hedgedoc` or `mysql://username:password@localhost:3306/hedgedoc`| Set the +db in URL style. If set, then the relevant `db` config entries will be overridden. | | `loglevel` +| `CMD_LOGLEVEL` | **`info`**, `debug` ... | Defines what kind of logs are provided to stdout. Available +options: `debug`, `verbose`, `info`, `warn`, `error` +| | `forbiddenNoteIDs` | `CMD_FORBIDDEN_NOTE_IDS` +| **`['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views']`** +, `['robots.txt']` or `'robots.txt'` | disallow creation of notes, even if `allowFreeUrl` or `CMD_ALLOW_FREEURL` +is `true` +| | `imageUploadType` | `CMD_IMAGE_UPLOAD_TYPE` | **`filesystem`**, `imgur`, `s3`, `minio`, `azure`, `lutim` +| Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) +or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. | +| `sourceURL` | `CMD_SOURCE_URL` | **no default** +, `https://github.com/hedgedoc/hedgedoc/tree/` | Provides the link to the source code of +HedgeDoc on the entry page (Please, make sure you change this when you run a modified version) +| | `tooBusyLag` | `CMD_TOOBUSY_LAG` | **`70`** +| CPU time for one event loop tick until node throttles connections. (milliseconds) +| | `staticCacheTime` | | **`1 * 24 * 60 * 60 * 1000`** +| static file cache time | | `heartbeatInterval` | | **`5000`** +| socket.io heartbeat interval | | `heartbeatTimeout` | | **`10000`** +| socket.io heartbeat timeout | | `documentMaxLength` | | **`100000`** +| note max length | | `linkifyHeaderStyle` | | **`keep-case`**, `lower-case`, `gfm` +| how is a header text converted into a link id | + +## HedgeDoc paths stuff + +these are rarely used for various reasons. + +| config file | environment | ** +default** and example values | description | | ----------------- | ----------- | +----------------------------------------------------- | +------------------------------------------------------------------------------------------------ | | `defaultNotePath` | +| **`./public/default.md`** | default note file path1, empty notes will be +created with this template. | | `dhParamPath` | | **`undefined`**, `./cert/dhparam.pem` | SSL +dhparam path1 (only need when you set `useSSL`) | | `sslCAPath` | +| **`undefined`**, `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain1 (only need when you set `useSSL`) +| | `sslCertPath` | | **`undefined`**, `./cert/hedgedoc_io.crt` | SSL cert path1 (only need +when you set `useSSL`) | | `sslKeyPath` | | **`undefined`** +, `./cert/client.key` | SSL key path1 (only need when you set `useSSL`) +| | `tmpPath` | | **`os.tmpdir()`**, `./tmp/` | temp directory path1 +| | `docsPath` | | **`./public/docs`** | docs directory path1 +| | `viewPath` | | **`./public/views`** | template directory path1 +| | `uploadsPath` | | **`./public/uploads`** | uploads directory1 - needs +to be persistent when you use imageUploadType `filesystem` | + +**Note:** *relative paths are based on HedgeDoc's base directory* + +## HedgeDoc Location + +| config file | environment | ** +default** and example value | description | | ---------------- | --------------------- | +---------------------------------------------------------------- | +----------------------------------------------------------------------------------------------------------------- | +| `domain` | `CMD_DOMAIN` | **`null`**, `localhost`, `hedgedoc.org` | domain +name | | `urlPath` | `CMD_URL_PATH` | **`null`**, `hedgedoc` | +If HedgeDoc is run from a subdirectory like `www.example.com/` | +| `host` | `CMD_HOST` | **`0.0.0.0`**, `localhost` | +interface/ip to listen on | | `port` | `CMD_PORT` | **`3000`**, `80` +| port to listen on | | `path` | `CMD_PATH` | **no default**, `/var/run/hedgedoc.sock` +| path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) | +| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | **`false`** or `true` | set to +use SSL protocol for resources path (only applied when domain is set) | | `useSSL` +| | **`false`** or `true` | set to use SSL server (if `true`, will auto turn +on `protocolUseSSL`) | | `urlAddPort` | `CMD_URL_ADDPORT` +| **`false`** or `true` | set to add port on callback URL (ports `80` +or `443` won't be applied) (only applied when domain is set) | | `allowOrigin` | `CMD_ALLOW_ORIGIN` +| **`['localhost']`**, `['hedgedoc.org']`, `['localhost', 'hedgedoc.org']` | domain name whitelist (use comma to +separate) | + +## Web security aspects + +| config file | environment | ** +default** and example value | description | | -------------- | ----------------------------- | +------------------------------------------------------------------------------------------------------------------------------------------ +| +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `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) +| | | `CMD_HSTS_ENABLE` | **`true`** or `false` +| 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`** or `false` +| set to include subdomains in HSTS (default is `true`) +| | | `CMD_HSTS_MAX_AGE` | **`31536000`**, `60 * 60 * 24 * 365` +| max duration in seconds to tell clients to keep HSTS status (default is a year) +| | | `CMD_HSTS_PRELOAD` | **`true`** or `false` +| whether to allow preloading of the site's HSTS status (e.g. into browsers) +| | `csp` | +| `{"enable": true, "directives": {"scriptSrc": "trustworthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": true}` +| Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are passed to Helmet - +see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. Some defaults are +added to the configured values so that the application doesn't break. To disable this behaviour, set `addDefaults` +to `false`. Further, if `usecdn` is on, some CDN locations are allowed too. By default (`auto`), insecure (HTTP) +requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set `upgradeInsecureRequests` to +either `true` or `false`. | | | `CMD_CSP_ENABLE` | **`true`** or `false` +| whether to enable Content Security Policy (directives cannot be configured with environment variables) +| | | `CMD_CSP_REPORTURI` | **`undefined`**, `https://.report-uri.com/r/d/csp/enforce` +| Allows to add a URL for CSP reports in case of violations | | `cookiePolicy` | `CMD_COOKIE_POLICY` +| **`lax`**, `strict` or `none` +| Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without +https breaks the editor | + +## Privacy and External Requests + +| config file | environment | ** +default** and example value | description | | --------------- | -------------------- | ----------------------------- | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +| | `allowGravatar` | `CMD_ALLOW_GRAVATAR` | **`true`** or `false` | set to `false` to +disable [Libravatar](https://www.libravatar.org/) as profile picture source on your instance. Libravatar is a federated +open-source alternative to Gravatar. | | `useCDN` | `CMD_USECDN` | **`false`** or `true` | set to +use CDN resources or not (default is `false`) +| + +## Users and Privileges + +| config file | environment | ** +default** and example value | description | | --------------------- | --------------------------- | +----------------------------------------------------------------------- | +-------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `allowAnonymous` | `CMD_ALLOW_ANONYMOUS` | **`true`** or `false` +| Set to allow anonymous usage (default is `true`). | | `allowAnonymousEdits` | `CMD_ALLOW_ANONYMOUS_EDITS` +| **`false`** or `true` | If `allowAnonymous` is `false`: allow users +to select `freely` permission, allowing guests to edit existing notes (default is `false`). | | `allowFreeURL` +| `CMD_ALLOW_FREEURL` | **`false`** or `true` | Set to allow +new note creation by accessing a nonexistent note URL. This is the behavior familiar +from [Etherpad](https://github.com/ether/etherpad-lite). | | `defaultPermission` | `CMD_DEFAULT_PERMISSION` +| **`editable`**, `freely`, `limited`, `locked`, `protected` or `private` | Set notes default permission (only applied +on signed-in users). | | `sessionName` | | **`connect.sid`** +| Cookie session name. | | `sessionLife` | `CMD_SESSION_LIFE` | **`14 * 24 * 60 * 60 * 1000`** +, `1209600000` (14 days) | Cookie session life time in milliseconds. | | `sessionSecret` +| `CMD_SESSION_SECRET` | **`secret`** | Cookie session +secret used to sign the session cookie. If none is set, one will randomly generated on each startup, meaning all your +users will be logged out. | + +## Login methods + +### Email (local account) + +| config file | environment | ** +default** and example value | description | | -------------------- | -------------------------- | +----------------------------- | +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `email` | `CMD_EMAIL` | **`true`** or `false` | Set to allow email sign-in. The +default is `true`. | | `allowEmailRegister` | `CMD_ALLOW_EMAIL_REGISTER` | **`true`** or `false` | Set to allow +registration of new accounts using an email address. If set to `false`, you can still create accounts using the command +line - see `bin/manage_users` for details (In production mode, remember to run it with `NODE_ENV` set as `production` in +the enviroment). This setting has no effect if `email` or `CMD_EMAIL` is `false`. The default for `allowEmailRegister` +or `CMD_ALLOW_EMAIL_REGISTER` is `true`. | + +### Dropbox Login + +| config file | environment | ** +default** and example value | description | | ----------- | -------------------------- | +------------------------------------ | +------------------------------------------------------------------------------------------------------------------------------------------- +| | `dropbox` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret +obtained by the [Dropbox developer tools](https://www.dropbox.com/developers/apps) | | | `CMD_DROPBOX_CLIENTID` | ** +no default** | Dropbox API client id | | | `CMD_DROPBOX_CLIENTSECRET` | **no default** +| Dropbox API client secret | + +### Facebook Login + +| config file | environment | ** +default** and example value | description | | ----------- | --------------------------- | +------------------------------------ | +------------------------------------------------------------------------------------------------------------------------------------- +| | `facebook` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret +obtained by the [Facebook app console](https://developers.facebook.com/apps) | | | `CMD_FACEBOOK_CLIENTID` | **no +default** | Facebook API client id | | | `CMD_FACEBOOK_CLIENTSECRET` | **no default** +| Facebook API client secret | + +### GitHub Login + +| config file | environment | ** +default** and example value | description | | ----------- | ------------------------- | +------------------------------------ | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `github` | | `{clientID: ..., clientSecret: ...}` | An object containing the client ID and the client secret +obtained by the GitHub developer page. For more details have a look at the [GitHub auth guide](guides/auth/github.md). | +| | `CMD_GITHUB_CLIENTID` | **no default** | GitHub API client id | | +| `CMD_GITHUB_CLIENTSECRET` | **no default** | GitHub API client secret | + +### GitLab Login + +| config file | environment | ** +default** and example value | description | | ----------- | ------------------------- | +---------------------------------------------------------------------------- | +----------------------------------------------------------------------------------------------------------------------------------- +| | `gitlab` | | `{baseURL: ..., scope: ..., version: ..., clientID: ..., clientSecret: ...}` | An object containing +your GitLab application data. Refer to the [GitLab guide](guides/auth/gitlab-self-hosted.md) for more details! | | +| `CMD_GITLAB_SCOPE` | **no default**, `read_user` or `api` | GitLab API +requested scope (default is `api`) (GitLab snippet import/export need `api` scope) +| | | `CMD_GITLAB_BASEURL` | **no default** | GitLab +authentication endpoint, set to use other endpoint than GitLab.com (optional) +| | | `CMD_GITLAB_CLIENTID` | **no default** | GitLab +API client id | | | `CMD_GITLAB_CLIENTSECRET` | **no default** +| GitLab API client secret | | | `CMD_GITLAB_VERSION` | **`v4`** +| GitLab API version (v3 or v4) +| + +### Google Login + +| config file | environment | ** +default** and example value | description | | ----------- | ------------------------- | +------------------------------------------------------- | +------------------------------------------------------------------------------------------------------------------------------------ +| | `google` | | `{clientID: ..., clientSecret: ..., hostedDomain: ...}` | An object containing the client ID and the +client secret obtained by the [Google API console](https://console.cloud.google.com/apis) | | | `CMD_GOOGLE_CLIENTID` +| **no default** | Google API client id | | | `CMD_GOOGLE_CLIENTSECRET` | **no +default** | Google API client secret | | | `CMD_GOOGLE_HOSTEDDOMAIN` | **no +default**, `example.com` | Provided only if the user belongs to a hosted domain. default +is `undefined` | + +### LDAP Login + +| config file | environment | ** +default** and example value | description | | ----------- | --------------------------- | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| --------------------------------------------------------------------------------------------------------------- | +| `ldap` | +| `{providerName: ..., url: ..., bindDn: ..., bindCredentials: ..., searchBase: ..., searchFilter: ..., searchAttributes: ..., usernameField: ..., useridField: ..., tlsca: ...}` +| An object detailing the LDAP connection. Refer to the [LDAP-AD guide](guides/auth/ldap-ad.md) for more details! | | +| `CMD_LDAP_URL` | **no default**, `ldap://example.com` +| URL of LDAP server | | | `CMD_LDAP_BINDDN` | **no default** +| bindDn for LDAP access | | | `CMD_LDAP_BINDCREDENTIALS` | **no default**, | bindCredentials for LDAP access | | +| `CMD_LDAP_SEARCHBASE` | **no default**, `o=users,dc=example,dc=com` +| LDAP directory to begin search from | | | `CMD_LDAP_SEARCHFILTER` | **no default**, `(uid={{username}})` +| LDAP filter to search with | | | `CMD_LDAP_SEARCHATTRIBUTES` | **no default**, `displayName, mail` +| LDAP attributes to search with (use comma to separate) | | +| `CMD_LDAP_USERIDFIELD` | **no default**, `uidNumber` or `uid` or `sAMAccountName` +| The LDAP field which is used uniquely identify a user on HedgeDoc | | | `CMD_LDAP_USERNAMEFIELD` | **no default**, +fallback to userid | The LDAP field which is used as the username on HedgeDoc | | | `CMD_LDAP_TLS_CA` | **no +default**, `server-cert.pem, root.pem` +| Root CA for LDAP TLS in PEM format (use comma to separate) | | +| `CMD_LDAP_PROVIDERNAME` | **no default**, `My institution` +| Optional name to be displayed at login form indicating the LDAP provider | + +### Mattermost Login + +| config file | environment | ** +default** and example value | description | | ------------ | ----------------------------- | +-------------------------------------------------- | +--------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `mattermost` | | `{baseURL: ..., clientID: ..., clientSecret: ...}` | An object containing the base URL of your +Mattermost application data. Refer to the [Mattermost guide](guides/auth/mattermost-self-hosted.md) for more details! | +| | `CMD_MATTERMOST_BASEURL` | **no default** | Mattermost authentication +endpoint for versions below 5.0. For Mattermost version 5.0 and above, +see [guide](guides/auth/mattermost-self-hosted.md). | | | `CMD_MATTERMOST_CLIENTID` | **no default** +| Mattermost API client id | | | `CMD_MATTERMOST_CLIENTSECRET` | **no default** | +Mattermost API client secret | + +### OAuth2 Login + +| config file | environment | ** +default** and example value | description | | ----------- | ------------------------------------------- | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `oauth2` | +| `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ...}` +| An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) +or [Nextcloud](guides/auth/nextcloud.md) examples for more details! +| | | `CMD_OAUTH2_USER_PROFILE_URL` | **no default**, `https://example.com` +| Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to +the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more +details on all of the `CMD_OAUTH2...` options. | | | `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | **no default** +, `name` +| where to find the username in the JSON from the user profile URL. (no default value) +| | | `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | **no default**, `display-name` +| where to find the display-name in the JSON from the user profile URL. (no default value) +| | | `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | **no default**, `email` +| where to find the email address in the JSON from the user profile URL. (no default value) +| | | `CMD_OAUTH2_USER_PROFILE_ID_ATTR` | **no default**, `user_uuid` +| where to find the dedicated user ID (optional, overrides `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR`) +| | | `CMD_OAUTH2_TOKEN_URL` | **no default**, `https://example.com` +| sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) +| | | `CMD_OAUTH2_AUTHORIZATION_URL` | **no default**, `https://example.com` +| authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) +| | | `CMD_OAUTH2_CLIENT_ID` | **no default**, `afae02fckafd...` +| you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) +| | | `CMD_OAUTH2_CLIENT_SECRET` | **no default**, `afae02fckafd...` +| you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) +| | | `CMD_OAUTH2_PROVIDERNAME` | **no default**, `My institution` +| Optional name to be displayed at login form indicating the oAuth2 provider | | | `CMD_OAUTH2_SCOPE` +| **no default**, `openid email profile` +| Scope to request for OIDC (OpenID Connect) providers. | | | `CMD_OAUTH2_ROLES_CLAIM` | **no +default**, `roles` +| ID token claim, which is supposed to provide an array of strings of roles | | | `CMD_OAUTH2_ACCESS_ROLE` +| **no default**, `role/hedgedoc` +| The role which should be included in the ID token roles claim to grant access | + +### SAML Login + +| config file | environment | ** +default** and example value | description | | ----------- | --------------------------------------- | +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| +------------------------------------------------------------------------------------------------------------------------------------------------------ +| | `saml` | +| `{idpSsoUrl: ..., idpCert: ..., clientCert: ..., issuer: ..., identifierFormat: ..., disableRequestedAuthnContext: ..., groupAttribute: ..., externalGroups: [], requiredGroups: [], attribute: {id: ..., username: ..., email: ...}}` +| An object detailing your SAML provider. Refer to the [OneLogin](guides/auth/saml-onelogin.md) +and [SAML](guides/auth/saml.md) guides for more details! | | | `CMD_SAML_IDPSSOURL` | **no default** +, `https://idp.example.com/sso` +| authentication endpoint of IdP. for details, see [guide](guides/auth/saml-onelogin.md). | | | `CMD_SAML_IDPCERT` +| **no default**, `/path/to/cert.pem` +| certificate file path of IdP in PEM format | | | `CMD_SAML_CLIENTCERT` | **no default** +, `/path/to/privatecert.pem` +| certificate file path for the client in PEM format (optional) +| | | `CMD_SAML_ISSUER` | **no default** +| Issuer to supply to identity provider (optional, default: `serverURL` config)" +| | | `CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT` | **no default**, `true` or `false` +| true to allow any authentication method, false restricts to password authentication (PasswordProtectedTransport) +method (default: false) | | | `CMD_SAML_IDENTIFIERFORMAT` +| **`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`** +| name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) +| | | `CMD_SAML_GROUPATTRIBUTE` | **no default**, `memberOf` +| attribute name for group list (optional) +| | | `CMD_SAML_REQUIREDGROUPS` | **no default**, `hedgedoc-users` +| group names that allowed (use vertical bar to separate) (optional) +| | | `CMD_SAML_EXTERNALGROUPS` | **no default**, `Temporary-staff` +| group names that not allowed (use vertical bar to separate) (optional) +| | | `CMD_SAML_ATTRIBUTE_ID` | **no default**, `sAMAccountName` +| attribute map for `id` (optional, default: NameID of SAML response) +| | | `CMD_SAML_ATTRIBUTE_USERNAME` | **no default**, `mailNickname` +| attribute map for `username` (optional, default: NameID of SAML response) +| | | `CMD_SAML_ATTRIBUTE_EMAIL` | **no default**, `mail` +| attribute map for `email` (optional, default: NameID of SAML response if `CMD_SAML_IDENTIFIERFORMAT` is default) +| + +### Twitter Login + +| config file | environment | ** +default** and example value | description | | ----------- | ---------------------------- | +----------------------------------------- | +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +| | `twitter` | | `{consumerKey: ..., consumerSecret: ...}` | An object containing the consumer key and secret +obtained by the [Twitter developer tools](https://developer.twitter.com/apps). For more details have a look at +the [Twitter auth guide](guides/auth/twitter.md) | | | `CMD_TWITTER_CONSUMERKEY` | **no default** +| Twitter API consumer key | | | `CMD_TWITTER_CONSUMERSECRET` | **no default** | Twitter API +consumer secret | + +## Upload Storage + +These are only relevant when they are also configured in sync with their +`CMD_IMAGE_UPLOAD_TYPE`. Also keep in mind, that `filesystem` is available, so you don't have to use either of these. + +### Amazon S3 + +| config file | environment | ** +default** and example value | description | | ----------- | -------------------------- | +----------------------------------------------------------------------------------------------------------------- | +------------------------------------------------------------------------------------------------------------------------------------------ +| | `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](guides/s3-image-upload.md) | | | `CMD_S3_ACCESS_KEY_ID` | **no default** +| AWS access key id | | | `CMD_S3_SECRET_ACCESS_KEY` | **no default** +| AWS secret key | | | `CMD_S3_REGION` | **no default**, `ap-northeast-1` +| AWS S3 region | | `s3bucket` | `CMD_S3_BUCKET` | **no default** +| AWS S3 bucket name | | | `CMD_S3_ENDPOINT ENV` | **no default** +| S3 API endpoint if you don't use AWS name | + +### Azure Blob Storage + +| config file | environment | ** +default** and example value | description | | ----------- | ----------------------------- | +----------------------------- | ------------------------------------------------------------------------- | | +| `CMD_AZURE_CONNECTION_STRING` | **no default** | Azure Blob Storage connection string | | +| `CMD_AZURE_CONTAINER` | **no default** | Azure Blob Storage container name (automatically +created if non existent) | + +### imgur + +| config file | environment | **default** and example value | description | +| ----------- | -------------------- | ------------------------------ | ------------------- | +| | `CMD_IMGUR_CLIENTID` | **no default** | Imgur API client id | + +### Minio + +| config file | environment | ** +default** and example value | description | | ----------- | ---------------------- | +----------------------------------------------------------------------------------------------------------------------------------------- +| +----------------------------------------------------------------------------------------------------------------------------------------------- +| | `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 check out +our [Minio Image Upload Guide](guides/minio-image-upload.md) | | | `CMD_MINIO_ACCESS_KEY` | **no default** +| Minio access key | | | `CMD_MINIO_SECRET_KEY` | **no default** +| Minio secret key | | | `CMD_MINIO_ENDPOINT` | **no default**, `minio.example.org` +| Address of your Minio endpoint/instance | | | `CMD_MINIO_PORT` | **no default**, `9000` +| Port that is used for your Minio instance | | | `CMD_MINIO_SECURE` | **no default**, `true` +| If set to `true` HTTPS is used for Minio | + +### Lutim + +| config file | environment | ** +default** and example value | description | | ----------- | --------------- | ----------------------------- | +--------------------------------------------------------------------------- | | `lutim` | +| `{"url": "YOUR_LUTIM_URL"}` | When `imageUploadType` is set to `lutim`, you can setup the lutim url | | +| `CMD_LUTIM_URL` | **`https://framapic.org/`** | When `CMD_IMAGE_UPLOAD_TYPE` is set to `lutim`, you can setup the +lutim url | diff --git a/docs/dev/2.0.md b/docs/content/dev/2.0.md similarity index 100% rename from docs/dev/2.0.md rename to docs/content/dev/2.0.md diff --git a/docs/content/dev/api.md b/docs/content/dev/api.md new file mode 100644 index 000000000..960f53ef3 --- /dev/null +++ b/docs/content/dev/api.md @@ -0,0 +1,64 @@ +# API documentation + +Several tasks of HedgeDoc can be automated through HTTP requests. The available endpoints for this api are described in +this document. For code-autogeneration there is an OpenAPIv3-compatible description available [here](openapi.yml). + +## Notes + +These endpoints create notes, return information about them or export them. +You have to replace *\* with either the alias or id of a note you want to work on. + +| Endpoint | HTTP-Method | Description | +| ---------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `/new` | `GET` | **Creates a new +note.**
A random id will be assigned and the content will equal to the template (blank by default). After note creation a redirect is issued to the created note. | +| `/new` | `POST` | **Imports some markdown data into a new +note.**
A random id will be assigned and the content will equal to the body of the received HTTP-request. The `Content-Type: text/markdown` header should be set on this request. | +| `/new/` | `POST` | **Imports some markdown data into a new note with a + +given alias.**
This endpoint equals to the above one except that the alias from the url will be assigned to the note +if [FreeURL-mode](../configuration.md#users-and-privileges) is enabled. | | `//download` +or `/s//download` | `GET` | **Returns the raw markdown content of a note.** +| | `//publish` | `GET` | **Redirects to the published version of the note.** +| | `//slide` | `GET` | **Redirects to the slide-presentation of the +note.**
This is only useful on notes which are designed to be slides. | | `//info` +| `GET` | **Returns metadata about the note.**
This includes the title and description of the note as well as +the creation date and viewcount. The data is returned as a JSON object. | | `//revision` +| `GET` | **Returns a list of the available note revisions.**
The list is returned as a JSON object with an +array of revision-id and length associations. The revision-id equals to the timestamp when the revision was saved. | +| `//revision/` | `GET` | **Returns the revision of the note with some +metadata.**
The revision is returned as a JSON object with the content of the note and the authorship. | +| `//gist` | `GET` | **Creates a new GitHub Gist with the note's +content.**
If [GitHub integration](../configuration.md#github-login) is configured, the user will be redirected to +GitHub and a new Gist with the content of the note will be created. | + +## User / History + +These endpoints return information about the current logged-in user and it's note history. If no user is logged-in, the +most of this requests will fail with either a HTTP 403 or a JSON object containing `{"status":"forbidden"}`. + +| Endpoint | HTTP-Method | Description | +| ----------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/me` | `GET` | **Returns the profile data of the current logged-in +user.**
The data is returned as a JSON object containing the user-id, the user's name and a url to the profile picture. | +| `/me/export` | `GET` | **Exports a zip-archive with all notes of the current +user.** | +| `/history` | `GET` | **Returns a list of the last viewed +notes.**
The list is returned as a JSON object with an array containing for each entry it's id, title, tags, last visit time and pinned status. | +| `/history` | `POST` | **Replace user's history with a new +one.**
The body must be form-encoded and contain a field `history` with a JSON-encoded array like its returned from the server when exporting the history. | +| `/history` | `DELETE` | **Deletes the user's +history.** | +| `/history/` | `POST` | **Toggles the pinned status in the history for a +note.**
The body must be form-encoded and contain a field `pinned` that is either `true` or `false`. | +| `/history/` | `DELETE` | **Deletes a note from the user's +history.** | + +## HedgeDoc-server + +These endpoints return information about the running HedgeDoc instance. + +| Endpoint | HTTP-Method | Description | +| --------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/status` | `GET` | **Returns the current status of the HedgeDoc +instance.**
The data is returned as a JSON object containing the number of notes stored on the server, (distinct) online users and more. | diff --git a/docs/dev/db-schema.plantuml b/docs/content/dev/db-schema.plantuml similarity index 100% rename from docs/dev/db-schema.plantuml rename to docs/content/dev/db-schema.plantuml diff --git a/docs/content/dev/documentation.md b/docs/content/dev/documentation.md new file mode 100644 index 000000000..9c6ad5c56 --- /dev/null +++ b/docs/content/dev/documentation.md @@ -0,0 +1,37 @@ +# Documentation + +Our documentation is build with [mkdocs](https://www.mkdocs.org). + +## Writing + +All documentation files are found in the `docs/content` directory of +the [hedgedoc/hedgedoc repo](https://github.com/hedgedoc/hedgedoc). These files are just normal markdown files with +nothing special about them. + +The configuration for mkdocs lies in the `docs` folder in a file called `mkdocs.yml`. With that file the theme and menu + +- amoung others - can be configured. + **Please note:** Any new files need to be linked to by other files or put in the navigation or the files will be very + hard to find on the documentation website. + +## Building + +To build the documentation locally you need to perform the following steps: + +0. Make sure you have python3 installed. +1. Go into the `docs` folder. +2. Install all the dependencies (E.g. with a [venv](https://docs.python.org/3/library/venv.html)) + with `pip install -r requirements.txt` +3. Start the mkdocs dev server (`mkdocs serve`) or build the documentation (`mkdocs build`). + +## Deployment + +The documentation is deployed with [Messor Structor](https://github.com/traefik/structor). + +The necessary Dockerfile and version menu template and also the github action to build the whole documentation can be +found in the [docs.hedgedoc.org repo](https://github.com/hedgedoc/docs.hedgedoc.org). This repo is also used to deploy +the actuall website to github.io. + +Messor Structor builds and deploys the documentation by finding all branches that follow the pattern `v*`. For each +branch the docs are generated separately by first installing the dependencies from `requirements.txt` and then running +mkdocs. Afterwards the menu go template is used to include a version switcher in the theme. diff --git a/docs/dev/getting-started.md b/docs/content/dev/getting-started.md similarity index 69% rename from docs/dev/getting-started.md rename to docs/content/dev/getting-started.md index 9a35b71ab..77e3e180d 100644 --- a/docs/dev/getting-started.md +++ b/docs/content/dev/getting-started.md @@ -1,26 +1,24 @@ -Developer Notes -=== +# Getting started ## Preparing for running the code **Notice:** *There's [specialised instructions for docker](../setup/docker.md) or [heroku](../setup/heroku.md), if you prefer running code this way!* -1. Clone the repository with `git clone https://github.com/codimd/server.git codimd-server` +1. Clone the repository with `git clone https://github.com/hedgedoc/hedgedoc.git hedgedoc-server` (cloning is the preferred way, but you can also download and unzip a release) -2. Enter the directory and run `bin/setup`, which will install npm dependencies - and create configs. The setup script is written in Bash, you would need bash - as a prerequisite. -3. Setup the [config file](../configuration-config-file.md) or set up - [environment variables](../configuration-env-vars.md). +2. Enter the directory and run `bin/setup`, which will install npm dependencies and create configs. The setup script is + written in Bash, you would need bash as a prerequisite. + +3. Setup the [config file](../configuration.md) or set up + [environment variables](../configuration.md). ## Running the Code -Now that everything is in place, we can start CodiMD: - -4. `yarn run build` will build the frontend bundle. It uses webpack to do that. -5. Run the server with `node app.js` +Now that everything is in place, we can start HedgeDoc: +1. `yarn run build` will build the frontend bundle. It uses webpack to do that. +2. Run the server with `node app.js` ## Running the Code with Auto-Reload @@ -32,11 +30,10 @@ rebuild the frontend or restart the server if necessary. The commands will stay active in your terminal, so you will need multiple tabs to run both at the same time. -4. Use `yarn run dev` if you want webpack to continuously rebuild the frontend - code. -5. To auto-reload the server, the easiest method is to install [nodemon](https://www.npmjs.com/package/nodemon) - and run `nodemon --watch app.js --watch lib --watch locales app.js`. +1. Use `yarn run dev` if you want webpack to continuously rebuild the frontend code. +2. To auto-reload the server, the easiest method is to install [nodemon](https://www.npmjs.com/package/nodemon) + and run `nodemon --watch app.js --watch lib --watch locales app.js`. ## Structure @@ -44,7 +41,7 @@ The repository contains two parts: a server (backend) and a client (frontend). most of the server code is in `/lib` and most of the client code is in `public`. ```text -codimd-server/ +hedgedoc-server/ ├── docs/ --- documentation ├── lib/ --- server code ├── test/ --- test suite diff --git a/docs/content/dev/openapi.yml b/docs/content/dev/openapi.yml new file mode 100644 index 000000000..f0a180265 --- /dev/null +++ b/docs/content/dev/openapi.yml @@ -0,0 +1,456 @@ +openapi: 3.0.1 + +info: + title: HedgeDoc + description: HedgeDoc is an open source collaborative note editor. Several tasks of HedgeDoc can be automated through this API. + version: 1.7.1 + contact: + name: HedgeDoc on GitHub + url: https://github.com/hedgedoc/hedgedoc + license: + name: AGPLv3 + url: https://github.com/hedgedoc/hedgedoc/blob/master/LICENSE + +externalDocs: + url: https://github.com/hedgedoc/hedgedoc/blob/master/docs/dev/api.md + + +paths: + + /new: + get: + tags: + - note + summary: Creates a new note. + description: A random id will be assigned and the content will equal to the template (blank by default). After note creation a redirect is issued to the created note. + responses: + default: + description: Redirect to the new note + post: + tags: + - note + summary: Imports some markdown data into a new note. + description: A random id will be assigned and the content will equal to the body of the received HTTP-request. + requestBody: + required: true + description: The content of the note to be imported as markdown + content: + 'text/markdown': + example: '# Some header' + responses: + default: + description: Redirect to the imported note + + /new/{alias}: + post: + tags: + - note + summary: Imports some markdown data into a new note with a given alias. + description: 'This endpoint equals to the above one except that the alias from the url will be assigned to the note if [FreeURL-mode](../configuration-env-vars.md#users-and-privileges) is enabled.' + requestBody: + required: true + description: The content of the note to be imported as markdown + content: + 'text/markdown': + example: '# Some heading' + responses: + default: + description: Redirect to the imported note + parameters: + - name: alias + in: path + required: true + description: The alias for the note-id under which the note will be saved + content: + 'text/plain': + example: my-note + + /{note}/download: + get: + tags: + - note + summary: Returns the raw markdown content of a note. + responses: + 200: + description: The raw markdown content of the note + content: + 'text/markdown': + example: '# Some heading' + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note which should be downloaded + content: + 'text/plain': + example: my-note + + /{note}/publish: + get: + tags: + - note + summary: Redirects to the published version of the note. + responses: + default: + description: Redirect to the published version of the note + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note which should be published + content: + 'text/plain': + example: my-note + + /{note}/slide: + get: + tags: + - note + summary: Redirects to the slide-presentation of the note. + description: This is only useful on notes which are designed to be slides. + responses: + default: + description: Redirect to the slide version of the note + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note which should be shown as slide + content: + 'text/plain': + example: my-note + + /{note}/info: + get: + tags: + - note + summary: Returns metadata about the note. + description: This includes the title and description of the note as well as the creation date and viewcount. + responses: + 200: + description: Metadata about the note + content: + 'text/json': + schema: + type: object + properties: + title: + type: string + description: The title of the note + default: Untitled + description: + type: string + description: The description of the note or the first words from the note + viewcount: + type: integer + minimum: 0 + description: How often the published version of the note was viewed + createtime: + type: string + description: The timestamp when the note was created in ISO 8601 format. + updatetime: + type: string + description: The timestamp when the note was last updated in ISO 8601 format. + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note for which the info should be shown + content: + 'text/plain': + example: my-note + + /{note}/revision: + get: + tags: + - note + summary: Returns a list of the available note revisions. + description: The list is returned as a JSON object with an array of revision-id and length associations. The revision-id equals to the timestamp when the revision was saved. + responses: + 200: + description: Revisions of the note + content: + 'text/json': + schema: + type: object + properties: + revision: + type: array + description: Array that holds all revision-info objects + items: + type: object + properties: + time: + type: integer + description: UNIX-timestamp of when the revision was saved. Is also the revision-id. + length: + type: integer + description: Length of the document to the timepoint the revision was saved + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note for which revisions should be shown + content: + 'text/plain': + example: my-note + + /{note}/revision/{revision-id}: + get: + tags: + - note + summary: Returns the revision of the note with some metadata. + description: The revision is returned as a JSON object with the content of the note and the authorship. + responses: + 200: + description: Revision of the note for the given timestamp + content: + 'text/json': + schema: + type: object + properties: + content: + type: string + description: The raw markdown content of the note revision + authorship: + type: array + description: Data which gives insights about who worked on the note + items: + type: integer + description: Unique user ids and additional data + patch: + type: array + description: Data which gives insight about what changed in comparison to former revisions + items: + type: string + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note for which the revision should be shown + content: + 'text/plain': + example: my-note + - name: revision-id + in: path + required: true + description: The id (timestamp) of the revision to fetch + content: + 'text/plain': + example: 1570921051959 + + /{note}/gist: + get: + tags: + - note + summary: Creates a new GitHub Gist with the note's content. + description: 'If [GitHub integration](https://github.com/hedgedoc/hedgedoc/blob/master/docs/configuration-env-vars.md#github-login) is configured, the user will be redirected to GitHub and a new Gist with the content of the note will be created.' + responses: + default: + description: Redirect to the created gist (or the GitHub authentication before) + 404: + description: Note does not exist + parameters: + - name: note + in: path + required: true + description: The note which should be pasted to GitHub gist + content: + 'text/plain': + example: my-note + + /me: + get: + tags: + - user + summary: Returns the profile data of the current logged-in user. + description: The data is returned as a JSON object containing the user-id, the user's name and a url to the profile picture. Requires an active session of the user. + responses: + 200: + description: If the user is logged-in, the user data otherwise `{"status":"forbidden"}` + content: + 'text/json': + schema: + type: object + properties: + status: + type: string + description: ok if everything works as expected, forbidden is the user is not logged-in + id: + type: string + description: Unique id of the user + name: + type: string + description: The user's display name + photo: + type: string + description: An url to the online stored user profile photo + + /me/export: + get: + tags: + - user + summary: Exports a zip-archive with all notes of the current user. + responses: + default: + description: The zip-archive with all notes + + /history: + get: + tags: + - user + summary: Returns a list of the last viewed notes. + description: The list is returned as a JSON object with an array containing for each entry it's id, title, tags, last visit time and pinned status. + responses: + 200: + description: The list of recently viewed notes and pinned notes + content: + 'text/json': + schema: + type: object + properties: + history: + type: array + description: The array that contains history objects + items: + type: object + properties: + id: + type: string + description: The id or alias of the note + text: + type: string + description: The title of the note + time: + type: integer + description: The UNIX-timestamp when the note was last accessed by the user + tags: + type: array + description: The tags that were added by the user to the note + items: + type: string + pinned: + type: boolean + description: Whether the user has pinned this note + post: + tags: + - user + summary: Replace user's history with a new one. + description: The body must be form-encoded and contain a field `history` with a JSON-encoded array like its returned from the server when exporting the history. + requestBody: + required: true + content: + 'application/x-www-form-urlencoded': + example: 'history=[{"id":"example","text":"Untitled","time":1556275442010,"tags":[],"pinned":false}]' + responses: + 200: + description: History replaced + delete: + tags: + - user + summary: Deletes the user's history. + responses: + 200: + description: User's history deleted + + /history/{note}: + post: + tags: + - user + summary: Toggles the pinned status in the history for a note. + description: The body must be form-encoded and contain a field `pinned` that is either `true` or `false`. + requestBody: + required: true + content: + 'application/x-www-form-urlencoded': + example: 'pinned=false' + responses: + 200: + description: Pinned state toggled + parameters: + - name: note + in: path + required: true + description: The note for which the pinned state should be toggled + content: + 'text/plain': + example: my-note + delete: + tags: + - user + summary: Deletes a note from the user's history. + responses: + 200: + description: Pinned state toggled + parameters: + - name: note + in: path + required: true + description: The note for which the pinned state should be toggled + content: + 'text/plain': + example: my-note + + /status: + get: + tags: + - server + summary: Returns the current status of the HedgeDoc instance. + description: The data is returned as a JSON object containing the number of notes stored on the server, (distinct) online users and more. + responses: + 200: + description: The server info + content: + 'text/json': + schema: + type: object + properties: + onlineNotes: + type: integer + description: How many notes are edited at the moment + onlineUsers: + type: integer + description: How many users are online at the moment + distinctOnlineUsers: + type: integer + description: How many distinct users (different machines) are online at the moment + notesCount: + type: integer + description: How many notes are stored on the server + registeredUsers: + type: integer + description: How many users are registered on the server + onlineRegisteredUsers: + type: integer + description: How many of the online users are registered on the server + distinctOnlineRegisteredUsers: + type: integer + description: How many of the distinct online users are registered on the server + isConnectionBusy: + type: boolean + connectionSocketQueueLength: + type: integer + isDisconnectBusy: + type: boolean + disconnectSocketQueueLength: + type: integer + +tags: + - name: note + description: These endpoints create notes, return information about them or export them. + - name: user + description: These endpoints return information about the current logged-in user and it's note history. If no user is logged-in, the most of this requests will fail with either a HTTP 403 or a JSON object containing `{"status":"forbidden"}`. + - name: server + description: These endpoints return information about the running HedgeDoc instance. diff --git a/docs/dev/ot.md b/docs/content/dev/ot.md similarity index 100% rename from docs/dev/ot.md rename to docs/content/dev/ot.md diff --git a/docs/dev/public_api.yml b/docs/content/dev/public_api.yml similarity index 100% rename from docs/dev/public_api.yml rename to docs/content/dev/public_api.yml diff --git a/docs/dev/webpack.md b/docs/content/dev/webpack.md similarity index 100% rename from docs/dev/webpack.md rename to docs/content/dev/webpack.md diff --git a/docs/guides/auth/github.md b/docs/content/guides/auth/github.md similarity index 100% rename from docs/guides/auth/github.md rename to docs/content/guides/auth/github.md diff --git a/docs/guides/auth/gitlab-self-hosted.md b/docs/content/guides/auth/gitlab-self-hosted.md similarity index 100% rename from docs/guides/auth/gitlab-self-hosted.md rename to docs/content/guides/auth/gitlab-self-hosted.md diff --git a/docs/guides/auth/keycloak.md b/docs/content/guides/auth/keycloak.md similarity index 100% rename from docs/guides/auth/keycloak.md rename to docs/content/guides/auth/keycloak.md diff --git a/docs/guides/auth/ldap-AD.md b/docs/content/guides/auth/ldap-ad.md similarity index 89% rename from docs/guides/auth/ldap-AD.md rename to docs/content/guides/auth/ldap-ad.md index e74121f10..b7d0284e5 100644 --- a/docs/guides/auth/ldap-AD.md +++ b/docs/content/guides/auth/ldap-ad.md @@ -1,9 +1,8 @@ -AD LDAP auth -=== +# AD LDAP auth -To setup your CodiMD instance with Active Directory you need the following configs: +To setup your HedgeDoc instance with Active Directory you need the following configs: -``` +```env CMD_LDAP_URL=ldap://internal.example.com CMD_LDAP_BINDDN=cn=binduser,cn=Users,dc=internal,dc=example,dc=com CMD_LDAP_BINDCREDENTIALS= @@ -13,7 +12,6 @@ CMD_LDAP_USERIDFIELD=sAMAccountName CMD_LDAP_PROVIDERNAME=Example Inc AD ``` - `CMD_LDAP_BINDDN` is either the `distinguishedName` or the `userPrincipalName`. *This can cause "username/password is invalid" when either this value or the password from `CMD_LDAP_BINDCREDENTIALS` are incorrect.* `CMD_LDAP_SEARCHFILTER` matches on all users and uses either the email address or the `sAMAccountName` (usually the login name you also use to login to Windows). @@ -24,7 +22,6 @@ CMD_LDAP_PROVIDERNAME=Example Inc AD `CMD_LDAP_PROVIDERNAME` just the name written above the username and password field on the login page. - Same in json: ```json @@ -38,4 +35,4 @@ Same in json: }, ``` -More details and example: https://www.npmjs.com/package/passport-ldapauth +More details and example: diff --git a/docs/guides/auth/mattermost-self-hosted.md b/docs/content/guides/auth/mattermost-self-hosted.md similarity index 100% rename from docs/guides/auth/mattermost-self-hosted.md rename to docs/content/guides/auth/mattermost-self-hosted.md diff --git a/docs/guides/auth/nextcloud.md b/docs/content/guides/auth/nextcloud.md similarity index 100% rename from docs/guides/auth/nextcloud.md rename to docs/content/guides/auth/nextcloud.md diff --git a/docs/guides/auth/oauth.md b/docs/content/guides/auth/oauth.md similarity index 100% rename from docs/guides/auth/oauth.md rename to docs/content/guides/auth/oauth.md diff --git a/docs/content/guides/auth/saml-keycloak.md b/docs/content/guides/auth/saml-keycloak.md new file mode 100644 index 000000000..2745087cd --- /dev/null +++ b/docs/content/guides/auth/saml-keycloak.md @@ -0,0 +1,143 @@ +# How to setup HedgeDoc SAML with Keycloak + +## Configuring Keycloak + +### Get the public certificate + +1. Select the Realm you want to use for your HedgeDoc SAML +2. Select "Realm Settings" in left sidebar +3. Select the "Keys" tab +4. Click the button "Certificate" at `RS256` algorithm + ![keycloak_idp_cert](../../images/auth/keycloak_idp_cert.png) +5. Copy this key and save it to the file specified in `saml.idpCert` property of the HedgeDoc configuration + or `CMD_SAML_IDPCERT` environment variable + +### Create a new client + +1. Select "Client" in left sidebar + ![keycloak_clients_overview](../../images/auth/keycloak_clients_overview.png) +2. Click on the "Create" button +3. Set a Client ID and specify this in `saml.issuer` property of the HedgeDoc configuration or `CMD_SAML_ISSUER` + environment variable +4. Select `SAML` as Client Protocol +5. Set Client SAML Endpoint to `https://hedgedoc.example.com/auth/saml` (replace `https://hedgedoc.example.com` with the + base URL of your HedgeDoc installation) + ![keycloak_add_client](../../images/auth/keycloak_add_client.png) +6. Leave "Client Signature Required" enabled +7. Set Root URL to `https://hedgedoc.example.com` (replace it here also with the base URL of your HedgeDoc installation) +8. Set Valid Redirect URIs to `https://hedgedoc.example.com/auth/saml/callback` (you should also define all other + domains of your HedgeDoc installtion with the suffix `/auth/saml/callback`) +9. Set Base URL to `/` + ![keycloak_client_overview](../../images/auth/keycloak_client_overview.png) +10. _(optional)_ You can set which Name ID Format should be used + +## Configure HedgeDoc + +### Config file + +You have to put the following block inside your `config.json`: + +```json +"saml": { + "issuer": "hedgedoc", // Change to the "Client ID" specified in the Keycloak Client + "identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "idpSsoUrl": "https://keycloak.example.org/auth/realms/test/protocol/saml", // replace keycloak.example.org with the url of your keycloak server + "idpCert": "/path/to/the/cert.pem", + "clientCert": "/path/to/the/key.pem" // this one is optional, see below +} +``` + +### Environment Variables + +- `CMD_SAML_IDPSSOURL`: `https://keycloak.example.org/auth/realms/test/protocol/saml` (replace keycloak.example.org with + the url of your keycloak server) +- `CMD_SAML_IDPCERT`: `/path/to/the/cert.pem` +- *(optional, see below)* `CMD_SAML_CLIENTCERT`: `/path/to/the/key.pem` +- `CMD_SAML_ISSUER`: `hedgedoc` (Change to the "Client ID" specified in the Keycloak Client) +- `CMD_SAML_IDENTIFIERFORMAT`: `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` + +## Client certificate *(optional)* + +If you want keycloak to be able to verify HedgeDoc, you hava to create a client certificate. There are two options for +this: + +### Create Private Keys for Signing + +1. Generate the private key and certificate with the following commands: + +```shell +openssl genrsa -out priv.pem 2048 +openssl req -new -x509 -key priv.pem -out cert.pem +``` + +*execute the following steps in keycloak* + +2. Select "Client" in left sidebar +3. Go to your HedgeDoc-Client +4. Select the "SAML Keys" tab + ![keycloak_saml_import_cert](../../images/auth/keycloak_saml_import_cert.png) +5. Click on "Import" +6. Select `Certificate PEM` as "Archive Format" +7. Now upload the generated cert.pem (in this case named `cert.pem`) + ![keycloak_saml_import_cert_details](../../images/auth/keycloak_saml_import_cert_details.png) +8. Click on "Import" +9. Move or copy this key (in this case named `key.pem`) and save it to the file specified in `saml.clientCert` property + of the HedgeDoc configuration or in the enviroment-variable `CMD_SAML_CLIENTCERT` + +### Convert Private Certificate generated by KeyCloak + +Instead if generating you own certificate, you can also use the one generated by keycloak. + +1. Select "Client" in left sidebar +2. Go to your HedgeDoc-Client +3. Select the "SAML Keys" tab + ![keycloak_saml_export_cert](../../images/auth/keycloak_saml_export_cert.png) + +5. Now click on "Export" +6. Here you can select the output format, choose `PKCS12`. You also have to set a password. Choose your own. + ![keycloak_saml_export_cert_details](../../images/auth/keycloak_saml_export_cert_details.png) +6. Click on "Download" and save the file somewhere on you computer +7. You now have to extract the private Key. You can do this with the following command. WHen asked, enter your password. + +```shell +openssl pkcs12 -in keystore.p12 -out key.pem -nocerts -nodes +``` + +8. Move or copy this key (in this case named `key.pem`) and save it to the file specified in `saml.idpCert` property of + the HedgeDoc configuration or in the enviroment-variable `CMD_SAML_CLIENTCERT` + +## Use Persistent Identifiers + +Instead of using the username as the owner-key in the HedgeDoc database, you can also use a persistent identifier. This +allows to change the username, without them loosing access to their notes. + +1. Go to the HedgeDoc-Client in keycloak. Now enable the option "Force Name ID Format" and select "persistent" as the " + Name ID Format". + ![keycloak_force_idformat](../../images/auth/keycloak_force_idformat.png) +2. For HedgeDoc to be able to use the username and email configured in keycloak, you have to create the following SAML + protocol mappers: + 2.1. Create a mapper with the type `User Property`. Set the Name, Property and SAML Attribute Name to `username`. Now + you can specify a friendly name (for example `Username`) + ![keycloak_mapper_username](../../images/auth/keycloak_mapper_username.png) + 2.2 Create a mapper with the type `User Property`. Set the Name, Property and SAML Attribute Name to `email`. Now you + can specify a friendly name (for example `E-Mail`) + ![keycloak_mapper_email](../../images/auth/keycloak_mapper_email.png) + +The configured mappers should look like this: +![keycloak_mapper_overview](../../images/auth/keycloak_mapper_overview.png) + +3. You now have to add the following block to the saml-definition inside your `config.json`: + +```json +"attribute": { + "username": "username" + "email": "email", +} +``` + +It you configure HedgeDoc with enviroment variables, these are the ones you have to set: + +```bash +CMD_SAML_ATTRIBUTE_USERNAME=username +CMD_SAML_ATTRIBUTE_EMAIL=email +``` diff --git a/docs/guides/auth/saml-onelogin.md b/docs/content/guides/auth/saml-onelogin.md similarity index 100% rename from docs/guides/auth/saml-onelogin.md rename to docs/content/guides/auth/saml-onelogin.md diff --git a/docs/guides/auth/saml.md b/docs/content/guides/auth/saml.md similarity index 100% rename from docs/guides/auth/saml.md rename to docs/content/guides/auth/saml.md diff --git a/docs/guides/auth/twitter.md b/docs/content/guides/auth/twitter.md similarity index 100% rename from docs/guides/auth/twitter.md rename to docs/content/guides/auth/twitter.md diff --git a/docs/guides/migrate-etherpad.md b/docs/content/guides/migrate-etherpad.md similarity index 100% rename from docs/guides/migrate-etherpad.md rename to docs/content/guides/migrate-etherpad.md diff --git a/docs/guides/migrations-and-breaking-changes.md b/docs/content/guides/migrations-and-breaking-changes.md similarity index 100% rename from docs/guides/migrations-and-breaking-changes.md rename to docs/content/guides/migrations-and-breaking-changes.md diff --git a/docs/guides/minio-image-upload.md b/docs/content/guides/minio-image-upload.md similarity index 100% rename from docs/guides/minio-image-upload.md rename to docs/content/guides/minio-image-upload.md diff --git a/docs/guides/providing-terms.md b/docs/content/guides/providing-terms.md similarity index 100% rename from docs/guides/providing-terms.md rename to docs/content/guides/providing-terms.md diff --git a/docs/guides/s3-image-upload.md b/docs/content/guides/s3-image-upload.md similarity index 97% rename from docs/guides/s3-image-upload.md rename to docs/content/guides/s3-image-upload.md index 7ca8dd12f..626119dac 100644 --- a/docs/guides/s3-image-upload.md +++ b/docs/content/guides/s3-image-upload.md @@ -77,7 +77,7 @@ Guide - Setup CodiMD S3 image upload } ``` -9. In additional to edit `config.json` directly, you could also try [environment variables](../configuration-env-vars.md). +9. In additional to edit `config.json` directly, you could also try [environment variables](../configuration.md). ## Related Tools diff --git a/docs/history.md b/docs/content/history.md similarity index 100% rename from docs/history.md rename to docs/content/history.md diff --git a/docs/images/auth/application-page.png b/docs/content/images/auth/application-page.png similarity index 100% rename from docs/images/auth/application-page.png rename to docs/content/images/auth/application-page.png diff --git a/docs/images/auth/create-oauth-app.png b/docs/content/images/auth/create-oauth-app.png similarity index 100% rename from docs/images/auth/create-oauth-app.png rename to docs/content/images/auth/create-oauth-app.png diff --git a/docs/images/auth/create-twitter-app.png b/docs/content/images/auth/create-twitter-app.png similarity index 100% rename from docs/images/auth/create-twitter-app.png rename to docs/content/images/auth/create-twitter-app.png diff --git a/docs/images/auth/gitlab-application-details.png b/docs/content/images/auth/gitlab-application-details.png similarity index 100% rename from docs/images/auth/gitlab-application-details.png rename to docs/content/images/auth/gitlab-application-details.png diff --git a/docs/images/auth/gitlab-new-application.png b/docs/content/images/auth/gitlab-new-application.png similarity index 100% rename from docs/images/auth/gitlab-new-application.png rename to docs/content/images/auth/gitlab-new-application.png diff --git a/docs/images/auth/gitlab-sign-in.png b/docs/content/images/auth/gitlab-sign-in.png similarity index 100% rename from docs/images/auth/gitlab-sign-in.png rename to docs/content/images/auth/gitlab-sign-in.png diff --git a/docs/content/images/auth/keycloak_add_client.png b/docs/content/images/auth/keycloak_add_client.png new file mode 100644 index 000000000..79121b15a Binary files /dev/null and b/docs/content/images/auth/keycloak_add_client.png differ diff --git a/docs/content/images/auth/keycloak_client_overview.png b/docs/content/images/auth/keycloak_client_overview.png new file mode 100644 index 000000000..1ff9d9864 Binary files /dev/null and b/docs/content/images/auth/keycloak_client_overview.png differ diff --git a/docs/content/images/auth/keycloak_clients_overview.png b/docs/content/images/auth/keycloak_clients_overview.png new file mode 100644 index 000000000..388b3e002 Binary files /dev/null and b/docs/content/images/auth/keycloak_clients_overview.png differ diff --git a/docs/content/images/auth/keycloak_force_idformat.png b/docs/content/images/auth/keycloak_force_idformat.png new file mode 100644 index 000000000..1b1bf3025 Binary files /dev/null and b/docs/content/images/auth/keycloak_force_idformat.png differ diff --git a/docs/content/images/auth/keycloak_idp_cert.png b/docs/content/images/auth/keycloak_idp_cert.png new file mode 100644 index 000000000..1b899283f Binary files /dev/null and b/docs/content/images/auth/keycloak_idp_cert.png differ diff --git a/docs/content/images/auth/keycloak_mapper_email.png b/docs/content/images/auth/keycloak_mapper_email.png new file mode 100644 index 000000000..b0ad667ad Binary files /dev/null and b/docs/content/images/auth/keycloak_mapper_email.png differ diff --git a/docs/content/images/auth/keycloak_mapper_overview.png b/docs/content/images/auth/keycloak_mapper_overview.png new file mode 100644 index 000000000..8402a0bc7 Binary files /dev/null and b/docs/content/images/auth/keycloak_mapper_overview.png differ diff --git a/docs/content/images/auth/keycloak_mapper_username.png b/docs/content/images/auth/keycloak_mapper_username.png new file mode 100644 index 000000000..ccaa89549 Binary files /dev/null and b/docs/content/images/auth/keycloak_mapper_username.png differ diff --git a/docs/content/images/auth/keycloak_saml_export_cert.png b/docs/content/images/auth/keycloak_saml_export_cert.png new file mode 100644 index 000000000..e9fa5722d Binary files /dev/null and b/docs/content/images/auth/keycloak_saml_export_cert.png differ diff --git a/docs/content/images/auth/keycloak_saml_export_cert_details.png b/docs/content/images/auth/keycloak_saml_export_cert_details.png new file mode 100644 index 000000000..7f4c9e0cc Binary files /dev/null and b/docs/content/images/auth/keycloak_saml_export_cert_details.png differ diff --git a/docs/content/images/auth/keycloak_saml_import_cert.png b/docs/content/images/auth/keycloak_saml_import_cert.png new file mode 100644 index 000000000..9295edabb Binary files /dev/null and b/docs/content/images/auth/keycloak_saml_import_cert.png differ diff --git a/docs/content/images/auth/keycloak_saml_import_cert_details.png b/docs/content/images/auth/keycloak_saml_import_cert_details.png new file mode 100644 index 000000000..bb9d1d6c0 Binary files /dev/null and b/docs/content/images/auth/keycloak_saml_import_cert_details.png differ diff --git a/docs/images/auth/mattermost-enable-oauth2.png b/docs/content/images/auth/mattermost-enable-oauth2.png similarity index 100% rename from docs/images/auth/mattermost-enable-oauth2.png rename to docs/content/images/auth/mattermost-enable-oauth2.png diff --git a/docs/images/auth/mattermost-oauth-app-add.png b/docs/content/images/auth/mattermost-oauth-app-add.png similarity index 100% rename from docs/images/auth/mattermost-oauth-app-add.png rename to docs/content/images/auth/mattermost-oauth-app-add.png diff --git a/docs/images/auth/mattermost-oauth-app-done.png b/docs/content/images/auth/mattermost-oauth-app-done.png similarity index 100% rename from docs/images/auth/mattermost-oauth-app-done.png rename to docs/content/images/auth/mattermost-oauth-app-done.png diff --git a/docs/images/auth/mattermost-oauth-app-form.png b/docs/content/images/auth/mattermost-oauth-app-form.png similarity index 100% rename from docs/images/auth/mattermost-oauth-app-form.png rename to docs/content/images/auth/mattermost-oauth-app-form.png diff --git a/docs/images/auth/nextcloud-oauth2-1-settings.png b/docs/content/images/auth/nextcloud-oauth2-1-settings.png similarity index 100% rename from docs/images/auth/nextcloud-oauth2-1-settings.png rename to docs/content/images/auth/nextcloud-oauth2-1-settings.png diff --git a/docs/images/auth/nextcloud-oauth2-2-client-add.png b/docs/content/images/auth/nextcloud-oauth2-2-client-add.png similarity index 100% rename from docs/images/auth/nextcloud-oauth2-2-client-add.png rename to docs/content/images/auth/nextcloud-oauth2-2-client-add.png diff --git a/docs/images/auth/nextcloud-oauth2-3-clientid-secret.png b/docs/content/images/auth/nextcloud-oauth2-3-clientid-secret.png similarity index 100% rename from docs/images/auth/nextcloud-oauth2-3-clientid-secret.png rename to docs/content/images/auth/nextcloud-oauth2-3-clientid-secret.png diff --git a/docs/images/auth/onelogin-add-app.png b/docs/content/images/auth/onelogin-add-app.png similarity index 100% rename from docs/images/auth/onelogin-add-app.png rename to docs/content/images/auth/onelogin-add-app.png diff --git a/docs/images/auth/onelogin-copy-idp-metadata.png b/docs/content/images/auth/onelogin-copy-idp-metadata.png similarity index 100% rename from docs/images/auth/onelogin-copy-idp-metadata.png rename to docs/content/images/auth/onelogin-copy-idp-metadata.png diff --git a/docs/images/auth/onelogin-edit-app-name.png b/docs/content/images/auth/onelogin-edit-app-name.png similarity index 100% rename from docs/images/auth/onelogin-edit-app-name.png rename to docs/content/images/auth/onelogin-edit-app-name.png diff --git a/docs/images/auth/onelogin-edit-sp-metadata.png b/docs/content/images/auth/onelogin-edit-sp-metadata.png similarity index 100% rename from docs/images/auth/onelogin-edit-sp-metadata.png rename to docs/content/images/auth/onelogin-edit-sp-metadata.png diff --git a/docs/images/auth/onelogin-select-template.png b/docs/content/images/auth/onelogin-select-template.png similarity index 100% rename from docs/images/auth/onelogin-select-template.png rename to docs/content/images/auth/onelogin-select-template.png diff --git a/docs/images/auth/onelogin-use-dashboard.png b/docs/content/images/auth/onelogin-use-dashboard.png similarity index 100% rename from docs/images/auth/onelogin-use-dashboard.png rename to docs/content/images/auth/onelogin-use-dashboard.png diff --git a/docs/images/auth/register-oauth-application-form.png b/docs/content/images/auth/register-oauth-application-form.png similarity index 100% rename from docs/images/auth/register-oauth-application-form.png rename to docs/content/images/auth/register-oauth-application-form.png diff --git a/docs/images/auth/register-twitter-application.png b/docs/content/images/auth/register-twitter-application.png similarity index 100% rename from docs/images/auth/register-twitter-application.png rename to docs/content/images/auth/register-twitter-application.png diff --git a/docs/images/auth/twitter-app-confirmation.png b/docs/content/images/auth/twitter-app-confirmation.png similarity index 100% rename from docs/images/auth/twitter-app-confirmation.png rename to docs/content/images/auth/twitter-app-confirmation.png diff --git a/docs/images/auth/twitter-app-keys.png b/docs/content/images/auth/twitter-app-keys.png similarity index 100% rename from docs/images/auth/twitter-app-keys.png rename to docs/content/images/auth/twitter-app-keys.png diff --git a/docs/content/images/favicon.png b/docs/content/images/favicon.png new file mode 100644 index 000000000..80afec65c Binary files /dev/null and b/docs/content/images/favicon.png differ diff --git a/docs/content/images/hedgedoc_logo_horizontal.svg b/docs/content/images/hedgedoc_logo_horizontal.svg new file mode 100644 index 000000000..e6dc0aa5d --- /dev/null +++ b/docs/content/images/hedgedoc_logo_horizontal.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/content/images/logo.svg b/docs/content/images/logo.svg new file mode 100644 index 000000000..c194df468 --- /dev/null +++ b/docs/content/images/logo.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/docs/images/minio-image-upload/create-bucket.png b/docs/content/images/minio-image-upload/create-bucket.png similarity index 100% rename from docs/images/minio-image-upload/create-bucket.png rename to docs/content/images/minio-image-upload/create-bucket.png diff --git a/docs/images/minio-image-upload/create-policy.png b/docs/content/images/minio-image-upload/create-policy.png similarity index 100% rename from docs/images/minio-image-upload/create-policy.png rename to docs/content/images/minio-image-upload/create-policy.png diff --git a/docs/images/minio-image-upload/default-view.png b/docs/content/images/minio-image-upload/default-view.png similarity index 100% rename from docs/images/minio-image-upload/default-view.png rename to docs/content/images/minio-image-upload/default-view.png diff --git a/docs/images/minio-image-upload/docker-logs.png b/docs/content/images/minio-image-upload/docker-logs.png similarity index 100% rename from docs/images/minio-image-upload/docker-logs.png rename to docs/content/images/minio-image-upload/docker-logs.png diff --git a/docs/images/minio-image-upload/open-edit-policy.png b/docs/content/images/minio-image-upload/open-edit-policy.png similarity index 100% rename from docs/images/minio-image-upload/open-edit-policy.png rename to docs/content/images/minio-image-upload/open-edit-policy.png diff --git a/docs/images/s3-image-upload/bucket-policy-editor.png b/docs/content/images/s3-image-upload/bucket-policy-editor.png similarity index 100% rename from docs/images/s3-image-upload/bucket-policy-editor.png rename to docs/content/images/s3-image-upload/bucket-policy-editor.png diff --git a/docs/images/s3-image-upload/bucket-property.png b/docs/content/images/s3-image-upload/bucket-property.png similarity index 100% rename from docs/images/s3-image-upload/bucket-property.png rename to docs/content/images/s3-image-upload/bucket-property.png diff --git a/docs/images/s3-image-upload/create-bucket.png b/docs/content/images/s3-image-upload/create-bucket.png similarity index 100% rename from docs/images/s3-image-upload/create-bucket.png rename to docs/content/images/s3-image-upload/create-bucket.png diff --git a/docs/images/s3-image-upload/custom-policy.png b/docs/content/images/s3-image-upload/custom-policy.png similarity index 100% rename from docs/images/s3-image-upload/custom-policy.png rename to docs/content/images/s3-image-upload/custom-policy.png diff --git a/docs/images/s3-image-upload/iam-user.png b/docs/content/images/s3-image-upload/iam-user.png similarity index 100% rename from docs/images/s3-image-upload/iam-user.png rename to docs/content/images/s3-image-upload/iam-user.png diff --git a/docs/images/s3-image-upload/review-policy.png b/docs/content/images/s3-image-upload/review-policy.png similarity index 100% rename from docs/images/s3-image-upload/review-policy.png rename to docs/content/images/s3-image-upload/review-policy.png diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 000000000..50ef5bc16 --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,25 @@ +# Welcome to the HedgeDoc Documentation + +![HedgeDoc Logo](images/hedgedoc_logo_horizontal.svg) + +HedgeDoc lets you create real-time collaborative markdown notes. You can test-drive it by visiting +our [HedgeDoc demo server][hedgedoc-demo]. + +It is inspired by Hackpad, Etherpad and similar collaborative editors. This project originated with the team +at [HackMD](https://hackmd.io) and now forked into its own +organisation. [A longer writeup can be read in the history doc](history.md) +or [you can have a look at an explanitory graph over at our website][hedgedoc-history]. + +If you have any questions that aren't answered here, feel free to ask us on [Matrix][matrix.org-url], stop by +our [community forums][hedgedoc-community] or have a look at our [FAQ][hedgedoc-faq]. + + +[hedgedoc-demo]: https://demo.hedgedoc.org + +[hedgedoc-history]: https://hedgedoc.org/history + +[hedgedoc-faq]: https://hedgedoc.org/faq + +[matrix.org-url]: https://chat.hedgedoc.org + +[hedgedoc-community]: https://community.hedgedoc.org diff --git a/docs/legal/developer-certificate-of-origin.txt b/docs/content/legal/developer-certificate-of-origin.txt similarity index 100% rename from docs/legal/developer-certificate-of-origin.txt rename to docs/content/legal/developer-certificate-of-origin.txt diff --git a/docs/setup/cloudron.md b/docs/content/setup/cloudron.md similarity index 100% rename from docs/setup/cloudron.md rename to docs/content/setup/cloudron.md diff --git a/docs/content/setup/docker-linuxserver.md b/docs/content/setup/docker-linuxserver.md new file mode 100644 index 000000000..cfdf8fd6c --- /dev/null +++ b/docs/content/setup/docker-linuxserver.md @@ -0,0 +1,26 @@ +# LinuxServer.io HedgeDoc Image + +[![Discord](https://img.shields.io/discord/354974912613449730.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=Discord&logo=discord)](https://discord.gg/YWrKVTn "realtime support / chat with the community and the team.") +[![GitHub Release](https://img.shields.io/github/release/linuxserver/docker-hedgedoc.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&logo=github)](https://github.com/linuxserver/docker-hedgedoc/releases) +[![GitHub Package Repository](https://img.shields.io/static/v1.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=linuxserver.io&message=GitHub%20Package&logo=github)](https://github.com/linuxserver/docker-hedgedoc/packages) +[![GitLab Container Registry](https://img.shields.io/static/v1.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=linuxserver.io&message=GitLab%20Registry&logo=gitlab)](https://gitlab.com/linuxserver.io/docker-hedgedoc/container_registry) +[![MicroBadger Layers](https://img.shields.io/microbadger/layers/linuxserver/hedgedoc.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge)](https://microbadger.com/images/linuxserver/hedgedoc "Get your own version badge on microbadger.com") +[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/hedgedoc.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=pulls&logo=docker)](https://hub.docker.com/r/linuxserver/hedgedoc) +[![Docker Stars](https://img.shields.io/docker/stars/linuxserver/hedgedoc.svg?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=stars&logo=docker)](https://hub.docker.com/r/linuxserver/hedgedoc) +[![Jenkins Build](https://img.shields.io/jenkins/build?labelColor=555555&logoColor=ffffff&style=for-the-badge&jobUrl=https%3A%2F%2Fci.linuxserver.io%2Fjob%2FDocker-Pipeline-Builders%2Fjob%2Fdocker-hedgedoc%2Fjob%2Fmain%2F&logo=jenkins)](https://ci.linuxserver.io/job/Docker-Pipeline-Builders/job/docker-hedgedoc/job/main/) +[![LSIO CI](https://img.shields.io/badge/dynamic/yaml?color=94398d&labelColor=555555&logoColor=ffffff&style=for-the-badge&label=CI&query=CI&url=https%3A%2F%2Fci-tests.linuxserver.io%2Flinuxserver%2Fhedgedoc%2Flatest%2Fci-status.yml)](https://ci-tests.linuxserver.io/linuxserver/hedgedoc/latest/index.html) + +[LinuxServer.io](https://linuxserver.io) have created an Ubuntu-based multi-arch container image for x86-64, arm64 and +armhf. + +- It supports all the environment variables detailed in the [configuration documentation](../configuration.md) to modify + it according to your needs. +- It gets rebuilt on new releases from HedgeDoc and also weekly if necessary to update any other package changes in the + underlying container, making it easy to keep your HedgeDoc instance up to date. +- It also details how to + easily [utilize Docker networking to reverse proxy](https://github.com/linuxserver/docker-hedgedoc/#application-setup) + HedgeDoc using their [SWAG docker image](https://github.com/linuxserver/docker-swag) + +In order to contribute check the LinuxServer.io [GitHub repository](https://github.com/linuxserver/docker-hedgedoc/) for +HedgeDoc. And to find all tags and versions of the image, check +the [Docker Hub repository](https://hub.docker.com/r/linuxserver/hedgedoc). diff --git a/docs/setup/docker.md b/docs/content/setup/docker.md similarity index 100% rename from docs/setup/docker.md rename to docs/content/setup/docker.md diff --git a/docs/setup/heroku.md b/docs/content/setup/heroku.md similarity index 100% rename from docs/setup/heroku.md rename to docs/content/setup/heroku.md diff --git a/docs/setup/kubernetes.md b/docs/content/setup/kubernetes.md similarity index 100% rename from docs/setup/kubernetes.md rename to docs/content/setup/kubernetes.md diff --git a/docs/setup/manual-setup.md b/docs/content/setup/manual-setup.md similarity index 100% rename from docs/setup/manual-setup.md rename to docs/content/setup/manual-setup.md diff --git a/docs/content/setup/reverse-proxy.md b/docs/content/setup/reverse-proxy.md new file mode 100644 index 000000000..8420a281b --- /dev/null +++ b/docs/content/setup/reverse-proxy.md @@ -0,0 +1,96 @@ +# Using a Reverse Proxy with HedgeDoc + +If you want to use a reverse proxy to serve HedgeDoc, here are the essential configs that you'll have to do. + +This documentation will cover HTTPS setup, with comments for HTTP setup. + +## HedgeDoc config + +[Full explanation of the configuration options](../configuration.md) + +| `config.json` parameter | Environment variable | Value | Example | +|-------------------------|----------------------|-------|---------| +| `domain` | `CMD_DOMAIN` | The full domain where your instance will be available | `hedgedoc.example.com` | +| `host` | `CMD_HOST` | An ip or domain name that is only available to HedgeDoc and your reverse proxy | `localhost` | +| `port` | `CMD_PORT` | An available port number on that IP | `3000` | +| `path` | `CMD_PATH` | path to UNIX domain socket to listen on (if specified, `host` or `CMD_HOST` and `port` or `CMD_PORT` are ignored) | `/var/run/hedgedoc.sock` | +| `protocolUseSSL` | `CMD_PROTOCOL_USESSL` | `true` if you want to serve your instance over SSL (HTTPS), `false` if you want to use plain HTTP | `true` | +| `useSSL` | | `false`, the communications between HedgeDoc and the proxy are unencrypted | `false` | +| `urlAddPort` | `CMD_URL_ADDPORT` | `false`, HedgeDoc should not append its port to the URLs it links | `false` | +| `hsts.enable` | `CMD_HSTS_ENABLE` | `true` if you host over SSL, `false` otherwise | `true` | + +## Reverse Proxy config + +### Generic + +The reverse proxy must allow websocket `Upgrade` requests at path `/sockets.io/`. + +It must pass through the scheme used by the client (http or https). + +### Nginx + +Here is an example configuration for Nginx. + +``` +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +server { + server_name hedgedoc.example.com; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /socket.io/ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + listen [::]:443 ssl http2; + listen 443 ssl http2; + ssl_certificate fullchain.pem; + ssl_certificate_key privkey.pem; + include options-ssl-nginx.conf; + ssl_dhparam ssl-dhparams.pem; +} +``` + +### Apache + +You will need these modules enabled: `proxy`, `proxy_http` and `proxy_wstunnel`. +Here is an example config snippet: + +``` + + ServerName hedgedoc.example.com + + RewriteEngine on + RewriteCond %{REQUEST_URI} ^/socket.io [NC] + RewriteCond %{HTTP:Upgrade} =websocket [NC] + RewriteRule /(.*) ws://127.0.0.1:3000/$1 [P,L] + + ProxyPass / http://127.0.0.1:3000/ + ProxyPassReverse / http://127.0.0.1:3000/ + + RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + SSLCertificateFile /etc/letsencrypt/live/hedgedoc.example.com/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/hedgedoc.example.com/privkey.pem + Include /etc/letsencrypt/options-ssl-apache.conf + +``` + diff --git a/docs/content/setup/yunohost.md b/docs/content/setup/yunohost.md new file mode 100644 index 000000000..05f0073e0 --- /dev/null +++ b/docs/content/setup/yunohost.md @@ -0,0 +1,9 @@ +YunoHost +=== + +HedgeDoc is available as a 1-click install on [YunoHost](https://yunohost.org/). YunoHost is a Debian GNU/Linux based +distribution packaged with free software that automates the installation of a personal web server. + +[![Install HedgeDoc with YunoHost](https://install-app.yunohost.org/install-with-yunohost.svg)](https://install-app.yunohost.org/?app=hedgedoc) + +The source code for the package can be found [here](https://github.com/YunoHost-Apps/hedgedoc_ynh). diff --git a/docs/slide-options.md b/docs/content/slide-options.md similarity index 100% rename from docs/slide-options.md rename to docs/content/slide-options.md diff --git a/docs/content/theme/styles/hedgedoc-color.css b/docs/content/theme/styles/hedgedoc-color.css new file mode 100644 index 000000000..319bf8302 --- /dev/null +++ b/docs/content/theme/styles/hedgedoc-color.css @@ -0,0 +1,14 @@ +[data-md-color-primary=hedgedoc] { + --md-primary-fg-color: #b51f08; + --md-primary-fg-color--light: #b51f08; + --md-primary-fg-color--dark: #b51f08; + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); +} + +[data-md-color-accent=hedgedoc] { + --md-accent-fg-color: #b51f08; + --md-accent-fg-color--transparent: hsla(348, 100%, 55%, 0.1); + --md-accent-bg-color: hsla(0, 0%, 100%, 1); + --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); +} diff --git a/docs/content/url-scheme.md b/docs/content/url-scheme.md new file mode 100644 index 000000000..c8b0ee7db --- /dev/null +++ b/docs/content/url-scheme.md @@ -0,0 +1,39 @@ +# URL scheme + +HedgeDoc has three different modes for viewing a stored note. Each mode has a slightly different URL for accessing it. +This document gives an overview about these URLs. +We assume that you replace `pad.example.com` with the domain of your instance. + +## Default (random) + +When you create a new note by clicking the "New note" button, your note is given a long random id and a random short-id. +The long id is needed for accessing the editor and the live-update view. The short-id is used for the "published" +version of a note that is read-only and does not update in realtime as well as for the presentation mode. + +| example URL | prefix | mode | content updates | +| -------------------------------------- | ------ | ----------------- | --------------- | +| pad.example.com/Ndmv3oCyREKZMjSGR9uhnQ | *none* | editor | in realtime | +| pad.example.com/s/ByXF7k-YI | s/ | read-only version | on reload | +| pad.example.com/p/ByXF7k-YI | p/ | presentation mode | on reload | + +## FreeURL mode + +If the setting `CMD_ALLOW_FREEURL` is enabled, users may create notes with a custom alias URL by just visiting the +editor version of a custom alias. The published version and the presentation mode may also be accessed with the custom +alias. + +| example URL | prefix | mode | content updates | +| --------------------------------- | ------ | ----------------- | --------------- | +| pad.example.com/my-awesome-note | *none* | editor | in realtime | +| pad.example.com/s/my-awesome-note | s/ | read-only version | on reload | +| pad.example.com/p/my-awesome-note | p/ | presentation mode | on reload | + +## Different editor modes + +The editor has three different sub-modes. All of these update the content in realtime. + +| example URL | icon in the navbar | behaviour | +| ------------------------------- | -------------------| ----------------------------------------------- | +| pad.example.com/longnoteid?edit | pencil | Full-screen markdown editor for the content | +| pad.example.com/longnoteid?view | eye | Full-screen view of the note without the editor | +| pad.example.com/longnoteid?both | columns | markdown editor and view mode side-by-side | diff --git a/docs/dev/api.md b/docs/dev/api.md deleted file mode 100644 index c27e1d4a6..000000000 --- a/docs/dev/api.md +++ /dev/null @@ -1,42 +0,0 @@ -# API documentation -Several tasks of CodiMD can be automated through HTTP requests. -The available endpoints for this api are described in this document. -For code-autogeneration there is an OpenAPIv3-compatible description available [here](openapi.yml). - -## Notes -These endpoints create notes, return information about them or export them. -You have to replace _\_ with either the alias or id of a note you want to work on. - -| Endpoint | HTTP-Method | Description | -|---|---|---| -| `/new` | `GET` | **Creates a new note.**
A random id will be assigned and the content will equal to the template (blank by default). After note creation a redirect is issued to the created note. | -| `/new` | `POST` | **Imports some markdown data into a new note.**
A random id will be assigned and the content will equal to the body of the received HTTP-request. The `Content-Type: text/markdown` header should be set on this request. | -| `/new/` | `POST` | **Imports some markdown data into a new note with a given alias.**
This endpoint equals to the above one except that the alias from the url will be assigned to the note if [FreeURL-mode](../configuration-env-vars.md#users-and-privileges) is enabled. | -| `//download` or `/s//download` | `GET` | **Returns the raw markdown content of a note.** | -| `//publish` | `GET` | **Redirects to the published version of the note.** | -| `//slide` | `GET` | **Redirects to the slide-presentation of the note.**
This is only useful on notes which are designed to be slides. | -| `//info` | `GET` | **Returns metadata about the note.**
This includes the title and description of the note as well as the creation date and viewcount. The data is returned as a JSON object. | -| `//revision` | `GET` | **Returns a list of the available note revisions.**
The list is returned as a JSON object with an array of revision-id and length associations. The revision-id equals to the timestamp when the revision was saved. | -| `//revision/` | `GET` | **Returns the revision of the note with some metadata.**
The revision is returned as a JSON object with the content of the note and the authorship. | -| `//gist` | `GET` | **Creates a new GitHub Gist with the note's content.**
If [GitHub integration](../configuration-env-vars.md#github-login) is configured, the user will be redirected to GitHub and a new Gist with the content of the note will be created. | - -## User / History -These endpoints return information about the current logged-in user and it's note history. If no user is logged-in, the most of this requests will fail with either a HTTP 403 or a JSON object containing `{"status":"forbidden"}`. - -| Endpoint | HTTP-Method | Description | -|---|---|---| -| `/me` | `GET` | **Returns the profile data of the current logged-in user.**
The data is returned as a JSON object containing the user-id, the user's name and a url to the profile picture. | -| `/me/export` | `GET` | **Exports a zip-archive with all notes of the current user.** | -| `/history` | `GET` | **Returns a list of the last viewed notes.**
The list is returned as a JSON object with an array containing for each entry it's id, title, tags, last visit time and pinned status. | -| `/history` | `POST` | **Replace user's history with a new one.**
The body must be form-encoded and contain a field `history` with a JSON-encoded array like its returned from the server when exporting the history. | -| `/history` | `DELETE` | **Deletes the user's history.** | -| `/history/` | `POST` | **Toggles the pinned status in the history for a note.**
The body must be form-encoded and contain a field `pinned` that is either `true` or `false`. -| `/history/` | `DELETE` | **Deletes a note from the user's history.** | - - -## CodiMD-server -These endpoints return information about the running CodiMD instance. - -| Endpoint | HTTP-Method | Description | -|---|---|---| -| `/status` | `GET` | **Returns the current status of the CodiMD instance.**
The data is returned as a JSON object containing the number of notes stored on the server, (distinct) online users and more. | diff --git a/docs/images/CodiMD-1.3.2-features.png b/docs/images/CodiMD-1.3.2-features.png deleted file mode 100644 index 952efe304..000000000 Binary files a/docs/images/CodiMD-1.3.2-features.png and /dev/null differ diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..d563115c5 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: HedgeDoc +site_url: https://docs.hedgedoc.org +repo_url: https://github.com/hedgedoc/hedgedoc +site_description: 'HedgeDoc Documentation' +site_author: 'HedgeDoc Developers' +docs_dir: content +nav: + - Home: index.md + - Installation: + - 'Manual Installation': setup/manual-setup.md + - 'Reverse Proxy': setup/reverse-proxy.md + - Docker: setup/docker.md + - Cloudron: setup/cloudron.md + - Heroku: setup/heroku.md + - LinuxServer: setup/docker-linuxserver.md + - Yunohost: setup/yunohost.md + - Guides: + - Authentication: + - LDAP: guides/auth/ldap-ad.md + - OAuth: guides/auth/oauth.md + - SAML: guides/auth/saml.md + - SAML Keycloak: guides/auth/saml-keycloak.md + - SAML Onelogin: guides/auth/saml-onelogin.md + - GitHub: guides/auth/github.md + - GitLab: guides/auth/gitlab-self-hosted.md + - Keycloak: guides/auth/keycloak.md + - NextCloud: guides/auth/nextcloud.md + - Twitter: guides/auth/twitter.md + - Migrate from Etherpad: guides/migrate-etherpad.md + - Breaking Changes: guides/migrations-and-breaking-changes.md + - Media Backend: + - Minion: guides/minio-image-upload.md + - S3: guides/s3-image-upload.md + - Setting Terms: guides/providing-terms.md + - Configuration: configuration.md + - Developer: + - 'Getting Started': dev/getting-started.md + - API: dev/api.md + - 'Operational Transformation': dev/ot.md + - Webpack: dev/webpack.md + - 'Documentation': dev/documentation.md + - FAQ: https://hedgedoc.org/faq +markdown_extensions: + - toc: + permalink: true +theme: + name: 'material' + language: en + favicon: images/favicon.png + logo: images/logo.svg + palette: + scheme: light + primary: 'hedgedoc' + accent: 'hedgedoc' + features: + - navigation.tabs + - navigation.sections + - toc.integrate + +extra_css: + - theme/styles/hedgedoc-color.css diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..57d08e48d --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +mkdocs==1.1.2 +mkdocs-material==6.2.3 +pymdown-extensions==8.1 diff --git a/docs/setup/docker-linuxserver.md b/docs/setup/docker-linuxserver.md deleted file mode 100644 index 2b6c09738..000000000 --- a/docs/setup/docker-linuxserver.md +++ /dev/null @@ -1,14 +0,0 @@ -LinuxServer.io CodiMD Image -=== -[![LinuxServer.io Discord](https://img.shields.io/discord/354974912613449730.svg?logo=discord&label=LSIO%20Discord&style=flat-square)](https://discord.gg/YWrKVTn)[![container version badge](https://images.microbadger.com/badges/version/linuxserver/codimd.svg)](https://microbadger.com/images/linuxserver/codimd "Get your own version badge on microbadger.com")[![container image size badge](https://images.microbadger.com/badges/image/linuxserver/codimd.svg)](https://microbadger.com/images/linuxserver/codimd "Get your own version badge on microbadger.com")![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/codimd.svg)![Docker Stars](https://img.shields.io/docker/stars/linuxserver/codimd.svg)[![Build Status](https://ci.linuxserver.io/buildStatus/icon?job=Docker-Pipeline-Builders/docker-codimd/master)](https://ci.linuxserver.io/job/Docker-Pipeline-Builders/job/docker-codimd/job/master/)[![LinuxServer.io CI summary](https://lsio-ci.ams3.digitaloceanspaces.com/linuxserver/codimd/latest/badge.svg)](https://lsio-ci.ams3.digitaloceanspaces.com/linuxserver/codimd/latest/index.html) - -[LinuxServer.io](https://linuxserver.io) have created an Ubuntu-based multi-arch container image for x86-64, arm64 and armhf. - -- It supports all the environment variables detailed in the [configuration documentation](../configuration-env-vars.md) to modify it according to your needs. - -- It gets rebuilt on new releases from CodiMD and also weekly if necessary to update any other package changes in the underlying container, making it easy to keep your CodiMD instance up to date. - -- It also details how to easily [utilize Docker networking to reverse proxy](https://github.com/linuxserver/docker-codimd/#application-setup) CodiMD using their [LetsEncrypt docker image](https://github.com/linuxserver/docker-letsencrypt) - -In order to contribute check the LinuxServer.io [GitHub repository](https://github.com/linuxserver/docker-codimd/) for CodiMD. -And to find all tags and versions of the image, check the [Docker Hub repository](https://hub.docker.com/r/linuxserver/codimd). diff --git a/locales/ar.json b/locales/ar.json deleted file mode 100644 index 88186f2d3..000000000 --- a/locales/ar.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "ملاحظات ماركداون تعاونية", - "Realtime collaborative markdown notes on all platforms.": "Collaborate on markdown notes on all platforms in realtime.", - "Best way to write and share your knowledge in markdown.": "The best platform to write and share markdown.", - "Intro": "مُقدِّمة", - "History": "التاريخ", - "New guest note": "ملاحظة ضيف جديد", - "Collaborate with URL": "Real time collaboration", - "Support charts and MathJax": "دعم المنحنيات البيانية و MathJax", - "Support slide mode": "دعم الشرائح التقديمية", - "Sign In": "لِج", - "Below is the history from browser": "Below is history from this 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": "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!": "Yes, do it!", - "Choose method": "اختر الطريقة", - "Sign in via %s": "لِج عبر %s", - "New": "جديد", - "Publish": "انشر", - "Extra": "إضافي", - "Revision": "مراجعة", - "Slide Mode": "نمط الشرائح التقديمية", - "Export": "تصدير", - "Import": "استيراد", - "Clipboard": "الحافظة", - "Download": "تنزيل", - "Raw HTML": "Raw 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 Metadata", - "Slide Example": "Slide Example", - "Cheatsheet": "Cheatsheet", - "Example": "مثال", - "Syntax": "Syntax", - "Header": "الترويسة", - "Unordered List": "قائمة غير مرتبة", - "Ordered List": "قائمة مرتبة", - "Todo List": "Checklist", - "Blockquote": "Blockquote", - "Bold font": "خط غامق", - "Italics font": "خط مائل", - "Strikethrough": "Strikethrough", - "Inserted text": "نص مُسَطَّر", - "Marked text": "نص بارز", - "Link": "الرابط", - "Image": "صورة", - "Code": "الشفرة", - "Externals": "Externals", - "This is a alert area.": "This is an alert area.", - "Revert": "تراجع", - "Import from clipboard": "استيراد من الحافظة", - "Paste your markdown or webpage here...": "Paste your markdown or webpage here…", - "Clear": "امسح", - "This note is locked": "هذه الملاحظة مقفلة", - "Sorry, only owner can edit this note.": "آسف ، يمكن للمالك فقط تعديل هذه الملاحظة.", - "OK": "حسنا", - "Reach the limit": "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...": "الصق عنوان 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": "تصدير بيانات المستخدم", - "Help us translating on %s": "ساعدنا في الترجمة على %s", - "Source Code": "الشفرة المصدرية", - "Register": "انشئ حسابا", - "Powered by %s": "مدعوم بـ %s", - "Help us translating": "ساعدنا في الترجمة", - "Join the community": "انضم إلى المجتمع", - "Imprint": "Imprint" -} \ No newline at end of file diff --git a/locales/ca.json b/locales/ca.json deleted file mode 100644 index 13520cb03..000000000 --- a/locales/ca.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Notes col·laboratives a Markdown", - "Realtime collaborative markdown notes on all platforms.": "Notes col·laboratives a Markdown per a totes les plataformes.", - "Best way to write and share your knowledge in markdown.": "La millor forma d'escriure i compartir el teu coneixement a Markdown.", - "Intro": "Introducció", - "History": "Història", - "New guest note": "Nova nota com a convidat", - "Collaborate with URL": "Col·laborar a través de URL", - "Support charts and MathJax": "Soport per a gràfics i MathJax", - "Support slide mode": "Soport per a diapositives", - "Sign In": "Entrar", - "Below is the history from browser": "A continuació es mostra l'historial del navegador", - "Welcome!": "Benvingut!", - "New note": "Nova nota", - "or": "o", - "Sign Out": "Sortir", - "Explore all features": "Explorar totes les funcions", - "Select tags...": "Seleccionar etiquetes...", - "Search keyword...": "Buscar paraules clau...", - "Sort by title": "Ordenar per títol", - "Title": "Títol", - "Sort by time": "Ordenar per hora", - "Time": "Tiempo", - "Export history": "Exportar historial", - "Import history": "Importar historial", - "Clear history": "Borrar historial", - "Refresh history": "Actualitzar historial", - "No history": "Cap historial", - "Import from browser": "Importar del navegador", - "Releases": "Versions", - "Are you sure?": "Estas segur?", - "Do you really want to delete this note?": "Estàs segur que vols eliminar aquesta nota?", - "All users will lose their connection.": "Tots els usuaris perdran la seva connexió.", - "Cancel": "Cancel·lar", - "Yes, do it!": "Si, fes-ho!", - "Choose method": "Triar mètode", - "Sign in via %s": "Entrar a través de %s", - "New": "Nou", - "Publish": "Publicar", - "Extra": "Extra", - "Revision": "Revisió", - "Slide Mode": "Mode presentació", - "Export": "Exportar", - "Import": "Importar", - "Clipboard": "Portapapers", - "Download": "Descarregar", - "Raw HTML": "HTML pur", - "Edit": "Editar", - "View": "Veure", - "Both": "Ambdós", - "Help": "Ajuda", - "Upload Image": "Pujar imatge", - "Menu": "Menú", - "This page need refresh": "Aquesta pàgina necessita ser refrescada", - "You have an incompatible client version.": "Tens una versió del client incompatible.", - "Refresh to update.": "Refrescar per actualitzar", - "New version available!": "Nova versió disponible!", - "See releases notes here": "Veure les notes de publicació aquí", - "Refresh to enjoy new features.": "Actualitzar per fer servir les noves funcions.", - "Your user state has changed.": "L'estat del teu usuari ha canviat.", - "Refresh to load new user state.": "Refrescar per actualitzar l'estat del teu usuari.", - "Refresh": "Refrescar", - "Contacts": "Contactes", - "Report an issue": "Reportar un problema", - "Meet us on %s": "Coneix-nos a %s", - "Send us email": "Enviar-nos un email", - "Documents": "Documents", - "Features": "Funcions", - "YAML Metadata": "Metadades de YAML", - "Slide Example": "Exemple de diapositiva", - "Cheatsheet": "Ajudamemories", - "Example": "Exemple", - "Syntax": "Sintaxis", - "Header": "Capçelera", - "Unordered List": "Llista desordenada", - "Ordered List": "Llista ordenada", - "Todo List": "Llista de tasques", - "Blockquote": "Bloc de cita", - "Bold font": "Font negreta", - "Italics font": "Font itàlica", - "Strikethrough": "Ratllat", - "Inserted text": "Text subrallat", - "Marked text": "Text marcat", - "Link": "Enllaç", - "Image": "Imatge", - "Code": "Codi", - "Externals": "Externs", - "This is a alert area.": "Això és una àrea d'alerta.", - "Revert": "Revertir", - "Import from clipboard": "Importar del portapapers", - "Paste your markdown or webpage here...": "Enganxa la teva markdown o pàgina web aquí...", - "Clear": "Netejar", - "This note is locked": "Aquesta nota està bloquejada", - "Sorry, only owner can edit this note.": "Perdona, només l'amo pot editar aquesta nota.", - "OK": "OK", - "Reach the limit": "Ha arribat al límit", - "Sorry, you've reached the max length this note can be.": "Perdona, ha arribat a la longitut màxima que pot tenir aquesta nota.", - "Please reduce the content or divide it to more notes, thank you!": "Siusplau, redueix el contingut o divideix-la en més notes, gràcies!", - "Import from Gist": "Importar d'un Gist", - "Paste your gist url here...": "Enganxa l'URL del teu Gist aquí...", - "Import from Snippet": "Importar d'Snippet", - "Select From Available Projects": "Triar d'un projecte disponsible", - "Select From Available Snippets": "Triar d'un Snippet disponible", - "OR": "O", - "Export to Snippet": "Exportar a Snippet", - "Select Visibility Level": "Triar el nivell de visibilitat", - "Night Theme": "Tema Fosc", - "Follow us on %s and %s.": "Segueix-nos a %s i %s", - "Privacy": "Privacitat", - "Terms of Use": "Condicions d'ús", - "Do you really want to delete your user account?": "Estàs segur que vols eliminar el teu compte?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Això esborrarà el teu compte, totes les teves notes i totes les teves referències cap al teu compte d'altres notes.", - "Delete user": "Esborrar compte", - "Export user data": "Exportar dades", - "Help us translating on %s": "Ajuda'ns traduïnt a %s", - "Source Code": "Codi Font", - "Register": "Registrar-se", - "Powered by %s": "Impulsat per %s", - "Help us translating": "Ajuda'ns traduïnt", - "Join the community": "Unir-se a la comunitat", - "Imprint": "Emprenta" -} \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json deleted file mode 100644 index 264c7abca..000000000 --- a/locales/cs.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "Collaborative markdown notes": "Kolaborativní markdown poznámky", - "Realtime collaborative markdown notes on all platforms.": "Spolupracujte na markdown poznámkách na všech platformách v reálném čase.", - "Best way to write and share your knowledge in markdown.": "Nejlepší platforma pro tvorbu a sdílení vašich znalostí v markdown.", - "Intro": "Intro", - "History": "Historie", - "New guest note": "Nová poznámka hosta", - "Collaborate with URL": "Spolupráce v reálném čase", - "Support charts and MathJax": "Funguje s grafy a MathJax", - "Support slide mode": "Podporuje režim prezentace", - "Sign In": "Přihlásit", - "Below is the history from browser": "Níže je historie z tohoto prohlížeče", - "Welcome!": "Vítejte!", - "New note": "Nová poznámka", - "or": "nebo", - "Sign Out": "Odhlásit", - "Explore all features": "Prozkoumat všechny funkce", - "Select tags...": "Zvolit štítky…", - "Search keyword...": "Vyhledat klíčové slovo …", - "Sort by title": "Seřadit podle názvu", - "Title": "Název", - "Sort by time": "Seřadit podle času", - "Time": "Čas", - "Export history": "Exportovat historii", - "Import history": "Importovat historii", - "Clear history": "Odstranit historii", - "Refresh history": "Aktualizovat historii", - "No history": "Žádná historie", - "Import from browser": "Importovat z prohlížeče", - "Releases": "Vydání", - "Are you sure?": "Jste si jisti?", - "Do you really want to delete this note?": "Opravdu chcete odstranit tuto poznámku?", - "All users will lose their connection.": "Všichni uživatelé ztratí spojení.", - "Cancel": "Storno", - "Yes, do it!": "Ano, provést!", - "Choose method": "Zvolit metodu", - "Sign in via %s": "Přihlásit se přes %s", - "New": "Nová", - "Publish": "Publikovat", - "Extra": "Extra", - "Revision": "Revize", - "Slide Mode": "Režim prezentace", - "Export": "Export", - "Import": "Import", - "Clipboard": "Schránka", - "Download": "Stáhnout", - "Raw HTML": "Raw HTML", - "Edit": "Editovat", - "View": "Zobrazit", - "Both": "Obojí", - "Help": "Nápověda", - "Upload Image": "Nahrát obrázek", - "Menu": "Menu", - "This page need refresh": "Tuto stránku je nutné znovu načíst", - "You have an incompatible client version.": "Verze vašeho klienta není kompatibilní.", - "Refresh to update.": "Znovu načíst a aktualizovat.", - "New version available!": "Je dostupná nová verze!", - "See releases notes here": "Viz poznámky k vydání zde", - "Refresh to enjoy new features.": "Znovu načíst a užít si nové funkce.", - "Your user state has changed.": "Váš uživatelský status se změnil.", - "Refresh to load new user state.": "Znovu načíst a nahrát nový uživatelský status.", - "Refresh": "Znovu načíst", - "Contacts": "Kontakty", - "Report an issue": "Nahlásit problém", - "Meet us on %s": "Potkejte se s námi na %s", - "Send us email": "Pošlete nám email", - "Documents": "Dokumenty", - "Features": "Funkce", - "YAML Metadata": "YAML metadata", - "Slide Example": "Příklad prezentace", - "Cheatsheet": "Tahák", - "Example": "Příklad", - "Syntax": "Syntaxe", - "Header": "Hlavička", - "Unordered List": "Nečíslovaný seznam", - "Ordered List": "Číslovaný seznam", - "Todo List": "Kontrolní seznam", - "Blockquote": "Citace", - "Bold font": "Tučně", - "Italics font": "Kurzívou", - "Strikethrough": "Přeškrtnuté", - "Inserted text": "Podtržený text", - "Marked text": "Zvýrazněný text", - "Link": "Odkaz", - "Image": "Obrázek", - "Code": "Kód", - "Externals": "Externí", - "This is a alert area.": "Toto je oblast upozornění.", - "Revert": "Vrátit", - "Import from clipboard": "Importovat ze schránky", - "Paste your markdown or webpage here...": "Sem vložte váš markdown nebo webovou stránku…", - "Clear": "Vyčistit", - "This note is locked": "Tato poznámka je uzamčena", - "Sorry, only owner can edit this note.": "Omlouvám se, pouze vlastník může editovat tuto poznámku.", - "OK": "OK", - "Reach the limit": "Dosažení limitu", - "Sorry, you've reached the max length this note can be.": "Omlouvám se, dosáhli jste maximální možné délky této poznámky.", - "Please reduce the content or divide it to more notes, thank you!": "Prosím zkraťte poznámku.", - "Import from Gist": "Importovat z Gist", - "Paste your gist url here...": "Sem vložte vaši gist url…", - "Import from Snippet": "Importovat ze Snippet", - "Select From Available Projects": "Zvolit z dostupných projektů", - "Select From Available Snippets": "Zvolit z dostupných úryvků", - "OR": "NEBO", - "Export to Snippet": "Exportovat do úryvku", - "Select Visibility Level": "Zvolit úroveň viditelnosti", - "Night Theme": "Noční téma", - "Follow us on %s and %s.": "Sledujte nás na %s, a %s.", - "Imprint": "Otisk", - "Privacy": "Soukromí", - "Terms of Use": "Podmínky použití", - "Do you really want to delete your user account?": "Opravdu chcete smazat váš uživatelský účet?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Tato operace odstraní váš účet, všechny poznámky, které vlastníte, a také odstraní všechny odkazy na váš účet z jiných poznámek.", - "Delete user": "Smazat uživatele", - "Export user data": "Exportovat uživatelská data", - "Source Code": "Zdrojový kód", - "Powered by %s": "Powered by %s", - "Register": "Registrovat", - "Help us translating": "Pomoci nám s překladem", - "Join the community": "Připojit ke komunitě" -} diff --git a/locales/da.json b/locales/da.json deleted file mode 100644 index 03c49ce51..000000000 --- a/locales/da.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "Kollaborative markdown-noter", - "Realtime collaborative markdown notes on all platforms.": "Kollaborative markdown-noter i realtid på alle platforme.", - "Best way to write and share your knowledge in markdown.": "Bedste måde at skrive og dele din viden i markdown.", - "Intro": "Intro", - "History": "Historik", - "New guest note": "Ny gæstenote", - "Collaborate with URL": "Samarbejd via URL", - "Support charts and MathJax": "Mulighed for diagrammer og MathJax", - "Support slide mode": "Mulighed for præsentationer", - "Sign In": "Log Ind", - "Below is the history from browser": "Forneden findes historikken fra browseren", - "Welcome!": "Velkommen!", - "New note": "Ny note", - "or": "eller", - "Sign Out": " Log Ud", - "Explore all features": "Udforsk alle features", - "Select tags...": "Vælg tags...", - "Search keyword...": "Søg nøgleord...", - "Sort by title": "Sortér titler", - "Title": "Titel", - "Sort by time": "Sortér kronologisk", - "Time": "Tid", - "Export history": "Eksportér historik", - "Import history": "Importér historik", - "Clear history": "Ryd hsitorik", - "Refresh history": "Genindlæs historik", - "No history": "Ingen historik", - "Import from browser": "Importér fra browser", - "Releases": "Releases", - "Are you sure?": "Er du sikker?", - "Cancel": "Afbryd", - "Yes, do it!": "Ja, gør det!", - "Choose method": "Vælg metode", - "Sign in via %s": "Log ind med %s", - "New": "Ny", - "Publish": "Publicér", - "Extra": "Ekstra", - "Revision": "Revision", - "Slide Mode": "Præsentationstilstand", - "Export": "Eksportér", - "Import": "Importér", - "Clipboard": "Udklipsholder", - "Download": "Download", - "Raw HTML": "Rå HTML", - "Edit": "Redigér", - "View": "Vis", - "Both": "Begge", - "Help": "Hjælp", - "Upload Image": "Upload billede", - "Menu": "Menu", - "This page need refresh": "Denne side skal genindlæses", - "You have an incompatible client version.": "Din klientversion er inkompatibel.", - "Refresh to update.": "Genindlæs for at opdatere.", - "New version available!": "Ny version tilgængelig!", - "See releases notes here": "Se release notes her", - "Refresh to enjoy new features.": "Genindlæs for at anvende ny funktionalitet.", - "Your user state has changed.": "Din brugerstatus er ændret.", - "Refresh to load new user state.": "Genindlæs for at anvende ny brugerstatus.", - "Refresh": "Genindlæs", - "Contacts": "Kontakter", - "Report an issue": "Rapportér en fejl", - "Send us email": "Send os en email", - "Documents": "Dokumenter", - "Features": "Funktionalitet", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Præsentationseksempel", - "Cheatsheet": "Snydeark", - "Example": "Eksempel", - "Syntax": "Syntaks", - "Header": "Overskrift", - "Unordered List": "Uordnet Liste", - "Ordered List": "Ordnet Liste", - "Todo List": "Tjekliste", - "Blockquote": "Blokcitat", - "Bold font": "Fed skrift", - "Italics font": "Kursiv skrift", - "Strikethrough": "Gennemstregning", - "Inserted text": "Indsat tekst", - "Marked text": "Markeret tekst", - "Link": "Link", - "Image": "Billede", - "Code": "Kode", - "Externals": "Eksterne", - "This is a alert area.": "Dette er et alarmområde.", - "Revert": "Fortryd ændringer", - "Import from clipboard": "Importér fra udklipsholder", - "Paste your markdown or webpage here...": "Indsæt din markdown eller hjemmeside her...", - "Clear": "Ryd", - "This note is locked": "Denne note er låst", - "Sorry, only owner can edit this note.": "Beklager, men kun ejeren kan redigere denne note.", - "OK": "Okay", - "Reach the limit": "Nå grænsen", - "Sorry, you've reached the max length this note can be.": "Beklager, du har nået grænsen for den maksimale længde denne note må være.", - "Please reduce the content or divide it to more notes, thank you!": "Vær venlig at begrænse indholdets mængde eller opdel det i flere noter, tak!", - "Import from Gist": "Importér fra Gist", - "Paste your gist url here...": "Indsæt din gist-url her...", - "Import from Snippet": "Importér fra Snippet", - "Select From Available Projects": "Vælg fra tilgængelige projekter", - "Select From Available Snippets": "Vælg fra tilgængelige Snippets", - "OR": "ELLER", - "Export to Snippet": "Eksportér til Snippet", - "Select Visibility Level": "Vælg synlighedsniveau" -} diff --git a/locales/de.json b/locales/de.json deleted file mode 100644 index dbdc68992..000000000 --- a/locales/de.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "Einleitung", - "History": "Verlauf", - "New guest note": "Neue Gastnotiz", - "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 aktualisieren.", - "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": "Datenschutz", - "Terms of Use": "Nutzungsbedingungen", - "Do you really want to delete your user account?": "Möchten Sie wirklich Ihr Nutzerkonto 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 Ihr Konto, alle Ihre Dokumente und alle Verweise auf Ihr Konto aus anderen Dokumenten.", - "Delete user": "Benutzer löschen", - "Export user data": "Exportiere Nutzerdaten", - "Help us translating on %s": "Hilf uns bei der Übersetzung auf %s", - "Source Code": "Quelltext", - "Register": "Registrieren", - "Powered by %s": "Betrieben mit %s", - "Help us translating": "Hilf uns beim Übersetzen", - "Join the community": "Tritt der Community bei", - "Imprint": "Impressum" -} \ No newline at end of file diff --git a/locales/el.json b/locales/el.json deleted file mode 100644 index 8f65dfa28..000000000 --- a/locales/el.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "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": "Eίσοδος", - "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": "Aκατέργαστο 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": "Στείλτε μας email", - "Documents": "Έγγραφα", - "Features": "Λειτουργία", - "YAML Metadata": "YAML μεταδεδομένα", - "Slide Example": "Σύρετε για παράδειγμα", - "Cheatsheet": "Σκονάκι", - "Example": "Παράδειγμα", - "Syntax": "Σύνταξη", - "Header": "Επικεφαλίδα", - "Unordered List": "Μη αριθμημένη λίστα", - "Ordered List": "Αριθμημένη λίστα", - "Todo List": "Todo List", - "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": "Εντάξει", - "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": "Εισαγωγή από Snippet", - "Select From Available Projects": "Eπιλογή από διαθέσιμα Projects", - "Select From Available Snippets": "Eπιλογή από διαθέσιμα Snippets", - "OR": "Ή", - "Export to Snippet": "Eξαγωγή σε Snippet", - "Select Visibility Level": "Επιλέξτε επίπεδο ορατότητας" -} diff --git a/locales/en.json b/locales/en.json deleted file mode 100644 index 66f0bceda..000000000 --- a/locales/en.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Collaborative markdown notes", - "Realtime collaborative markdown notes on all platforms.": "Collaborate on markdown notes on all platforms in realtime.", - "Best way to write and share your knowledge in markdown.": "The best platform to write and share markdown.", - "Intro": "Intro", - "History": "History", - "New guest note": "New guest note", - "Collaborate with URL": "Real time collaboration", - "Support charts and MathJax": "Works with charts and MathJax", - "Support slide mode": "Supports slide mode", - "Sign In": "Sign In", - "Below is the history from browser": "Below is history from this browser", - "Welcome!": "Welcome!", - "New note": "New note", - "or": "or", - "Sign Out": "Sign Out", - "Explore all features": "Explore all features", - "Select tags...": "Select tags…", - "Search keyword...": "Search keyword…", - "Sort by title": "Sort by title", - "Title": "Title", - "Sort by time": "Sort by time", - "Time": "Time", - "Export history": "Export history", - "Import history": "Import history", - "Clear history": "Clear history", - "Refresh history": "Refresh history", - "No history": "No history", - "Import from browser": "Import from browser", - "Releases": "Releases", - "Are you sure?": "Are you sure?", - "Do you really want to delete this note?": "Do you really want to delete this note?", - "All users will lose their connection.": "All users will lose their connection.", - "Cancel": "Cancel", - "Yes, do it!": "Yes, do it!", - "Choose method": "Choose method", - "Sign in via %s": "Sign in via %s", - "New": "New", - "Publish": "Publish", - "Extra": "Extra", - "Revision": "Revision", - "Slide Mode": "Slide Mode", - "Export": "Export", - "Import": "Import", - "Clipboard": "Clipboard", - "Download": "Download", - "Raw HTML": "Raw HTML", - "Edit": "Edit", - "View": "View", - "Both": "Both", - "Help": "Help", - "Upload Image": "Upload Image", - "Menu": "Menu", - "This page need refresh": "This page needs to be refreshed", - "You have an incompatible client version.": "Your client's version is incompatible.", - "Refresh to update.": "Refresh to update.", - "New version available!": "New version available!", - "See releases notes here": "See releases notes here", - "Refresh to enjoy new features.": "Refresh to enjoy new features.", - "Your user state has changed.": "Your user state has changed.", - "Refresh to load new user state.": "Refresh to load new user state.", - "Refresh": "Refresh", - "Contacts": "Contacts", - "Report an issue": "Report an issue", - "Meet us on %s": "Meet us on %s", - "Send us email": "Send us email", - "Documents": "Documents", - "Features": "Features", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Slide Example", - "Cheatsheet": "Cheatsheet", - "Example": "Example", - "Syntax": "Syntax", - "Header": "Header", - "Unordered List": "Unordered List", - "Ordered List": "Ordered List", - "Todo List": "Checklist", - "Blockquote": "Blockquote", - "Bold font": "Bold", - "Italics font": "Italicize", - "Strikethrough": "Strikethrough", - "Inserted text": "Underlined text", - "Marked text": "Highlighted text", - "Link": "Link", - "Image": "Image", - "Code": "Code", - "Externals": "Externals", - "This is a alert area.": "This is an alert area.", - "Revert": "Revert", - "Import from clipboard": "Import from clipboard", - "Paste your markdown or webpage here...": "Paste your markdown or webpage here…", - "Clear": "Clear", - "This note is locked": "This note is locked", - "Sorry, only owner can edit this note.": "Sorry, only the owner can edit this note.", - "OK": "OK", - "Reach the limit": "Reach the limit", - "Sorry, you've reached the max length this note can be.": "Sorry, you've reached the maximum length this note can be.", - "Please reduce the content or divide it to more notes, thank you!": "Please shorten the note.", - "Import from Gist": "Import from Gist", - "Paste your gist url here...": "Paste your gist url here…", - "Import from Snippet": "Import from Snippet", - "Select From Available Projects": "Select From Available Projects", - "Select From Available Snippets": "Select From Available Snippets", - "OR": "OR", - "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.", - "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", - "Help us translating on %s": "Help us translating on %s", - "Source Code": "Source Code", - "Register": "Register", - "Powered by %s": "Powered by %s", - "Help us translating": "Help us translating", - "Join the community": "Join the community", - "Imprint": "Imprint" -} \ No newline at end of file diff --git a/locales/eo.json b/locales/eo.json deleted file mode 100644 index c7c8cad26..000000000 --- a/locales/eo.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "Kunlaborataj marksubenaj notoj", - "Realtime collaborative markdown notes on all platforms.": "Tujkunlaborataj marksubenaj notoj ĉe ĉiuj sistemoj.", - "Best way to write and share your knowledge in markdown.": "La plej bona maniero skribi kaj havigi vian scion marksubene.", - "Intro": "Enkonduko", - "History": "Historio", - "New guest note": "Novan gastan noton", - "Collaborate with URL": "Kunlaboru per URL", - "Support charts and MathJax": "Ebleco por skemoj kaj MathJax", - "Support slide mode": "Ebleco por bildvica modo", - "Sign In": "Ensalutu", - "Below is the history from browser": "Malsupre estas la historio de la retumilo", - "Welcome!": "Bonvenon!", - "New note": "Novan Noton", - "or": "aŭ", - "Sign Out": "Elsalutu", - "Explore all features": "Esploru ĉiujn eblecojn", - "Select tags...": "Elektu etikedojn..", - "Search keyword...": "Serĉu ĉefvorton...", - "Sort by title": "Ordigu laŭ titolo", - "Title": "Titolo", - "Sort by time": "Ordigu laŭ tempo", - "Time": "Tempo", - "Export history": "Elportu historion", - "Import history": "Alportu historion", - "Clear history": "Malplenigu historion", - "Refresh history": "Refreŝigu historion", - "No history": "Neniu historio", - "Import from browser": "Alportu de retumilo", - "Releases": "Eldonoj", - "Are you sure?": "Ĉu vi certas?", - "Cancel": "Nuligu", - "Yes, do it!": "Jes, faru ĝin!", - "Choose method": "Elektu metodon", - "Sign in via %s": "Ensalutu per %s", - "New": "Nova", - "Publish": "Dissendu", - "Extra": "Plia", - "Revision": "Versio", - "Slide Mode": "Bildvica modo", - "Export": "Elportu", - "Import": "Alportu", - "Clipboard": "Poŝo", - "Download": "Elŝuti", - "Raw HTML": "Kruda HTML", - "Edit": "Redaktu", - "View": "Vidu", - "Both": "Ambaŭ", - "Help": "Helpo", - "Upload Image": "Alŝutu bildon", - "Menu": "Menuo", - "This page need refresh": "Ĉi tiu paĝo bezonas refreŝiĝi", - "You have an incompatible client version.": "Vi havas malkongruan klientversion.", - "Refresh to update.": "Refreŝigu por ĝisdatigi", - "New version available!": "Nova versio disponeblas!", - "See releases notes here": "Vidu elsendajn notojn ĉi tie", - "Refresh to enjoy new features.": "Refreŝigu por ĝui novajn eblecojn.", - "Your user state has changed.": "Via uzantstato ŝanĝiĝis.", - "Refresh to load new user state.": "Refreŝigu por ŝargi novan uzantstaton.", - "Refresh": "Refreŝigu", - "Contacts": "Kontaktuloj", - "Report an issue": "Raportu problemon", - "Send us email": "Sendu al ni retpoŝton", - "Documents": "Dosieroj", - "Features": "Eblecoj", - "YAML Metadata": "YAML metadateno", - "Slide Example": "Bildvica ekzemplo", - "Cheatsheet": "Gvidfolio", - "Example": "Ekzemplo", - "Syntax": "Sintakso", - "Header": "Paĝokapo", - "Unordered List": "Neordita Listo", - "Ordered List": "Ordita Listo", - "Todo List": "Farenda Listo", - "Blockquote": "Deŝovita cito", - "Bold font": "Dika tiparo", - "Italics font": "Kursiva tiparo", - "Strikethrough": "Trastrekita", - "Inserted text": "Enmetita teksto", - "Marked text": "Markita teksto", - "Link": "Ligilo", - "Image": "Bildo", - "Code": "Kodo", - "Externals": "Eksteraĵoj", - "This is a alert area.": "Ĉi tiu estas avertzono.", - "Revert": "Malfaru ŝanĝojn", - "Import from clipboard": "Alportu de la poŝo", - "Paste your markdown or webpage here...": "Algluu vian marksubenon aŭ retpaĝaron ĉi tie...", - "Clear": "Malplenigu", - "This note is locked": "Ĉi tiu noto estas ŝlosita", - "Sorry, only owner can edit this note.": "Bedaŭrinde, nur la proprulo povas redakti ĉi tiun noton.", - "OK": "Bone", - "Reach the limit": "Atingi la limigon", - "Sorry, you've reached the max length this note can be.": "Pardonon, ĉi tiu noto jam atingis maksimuman longecon.", - "Please reduce the content or divide it to more notes, thank you!": "Bonvolu malpligrandigi la enhavaĵon, aŭ dividi ĝin en pliajn notojn!", - "Import from Gist": "Alportu el Gist", - "Paste your gist url here...": "Algluu vian gist-an URL-n ĉi tie...", - "Import from Snippet": "Alportu el tekstero", - "Select From Available Projects": "Elektu el disponeblaj projektoj", - "Select From Available Snippets": "Elektu el disponeblaj teksteroj", - "OR": "AŬ", - "Export to Snippet": "Elportu al Snippet", - "Select Visibility Level": "Elektu videblecan nivelon" -} diff --git a/locales/es.json b/locales/es.json deleted file mode 100644 index 5bbb856bc..000000000 --- a/locales/es.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Notas colaborativas en Markdown", - "Realtime collaborative markdown notes on all platforms.": "Notas colaborativas en Markdown para todas las plataformas en tiempo real.", - "Best way to write and share your knowledge in markdown.": "La mejor forma de escribir y compartir tu conocimiento en Markdown.", - "Intro": "Introduccion", - "History": "Historia", - "New guest note": "Nueva nota como invitado", - "Collaborate with URL": "Colaborar via URL", - "Support charts and MathJax": "Soporte para gráficos y MathJax", - "Support slide mode": "Soporte para diapositivas", - "Sign In": "Ingresar", - "Below is the history from browser": "A continuación se muestra el historial del navegador", - "Welcome!": "¡Bienvenido!", - "New note": "Nueva nota", - "or": "o", - "Sign Out": "Salir", - "Explore all features": "Explorar todas las funciones", - "Select tags...": "Seleccionar etiquetas...", - "Search keyword...": "Buscar palabras clave...", - "Sort by title": "Ordenar por título", - "Title": "Título", - "Sort by time": "Ordenar por fecha", - "Time": "Tiempo", - "Export history": "Exportar historial", - "Import history": "Importar historial", - "Clear history": "Borrar historial", - "Refresh history": "Actualizar historial", - "No history": "Ningún historial", - "Import from browser": "Importar del navegador", - "Releases": "Versiones", - "Are you sure?": "¿Estás seguro?", - "Do you really want to delete this note?": "¿Realmente quieres eliminar esta nota?", - "All users will lose their connection.": "Todos los usuarios perderán su conexión.", - "Cancel": "Cancelar", - "Yes, do it!": "Si, ¡hazlo!", - "Choose method": "Elegir método", - "Sign in via %s": "Ingresar via %s", - "New": "Nuevo", - "Publish": "Publicar", - "Extra": "Extra", - "Revision": "Revision", - "Slide Mode": "Modo presentación", - "Export": "Exportar", - "Import": "Importar", - "Clipboard": "Portapapeles", - "Download": "Descargar", - "Raw HTML": "HTML puro", - "Edit": "Editar", - "View": "Ver", - "Both": "Ambos", - "Help": "Ayuda", - "Upload Image": "Subir imagen", - "Menu": "Menú", - "This page need refresh": "Esta página necesita ser cargada de nuevo", - "You have an incompatible client version.": "Tienes una version del cliente incompatible.", - "Refresh to update.": "Cargar de nuevo para actualizar.", - "New version available!": "¡Nueva versión disponible!", - "See releases notes here": "Ver aquí las notas de publicación", - "Refresh to enjoy new features.": "Actualizar para usar las nuevas funciones.", - "Your user state has changed.": "El estado de tu usuario ha cambiado.", - "Refresh to load new user state.": "Recargar para actualizar el estado de tu usuario.", - "Refresh": "Recargar", - "Contacts": "Contactos", - "Report an issue": "Reportar un problema", - "Meet us on %s": "Encuéntranos en %s", - "Send us email": "Enviarnos un email", - "Documents": "Documentos", - "Features": "Funciones", - "YAML Metadata": "Metadatos en YAML", - "Slide Example": "Ejemplo de diapositiva", - "Cheatsheet": "Ayudamemorias", - "Example": "Ejemplo", - "Syntax": "Sintaxis", - "Header": "Cabecera", - "Unordered List": "Lista desordenada", - "Ordered List": "Lista ordenada", - "Todo List": "Lista de tareas", - "Blockquote": "Bloque de cita", - "Bold font": "Fuente negrita", - "Italics font": "Fuente itálica", - "Strikethrough": "Tachado", - "Inserted text": "Texto subrayado", - "Marked text": "Texto marcado", - "Link": "Enlace", - "Image": "Imagen", - "Code": "Código", - "Externals": "Externos", - "This is a alert area.": "Esto es un área de alerta.", - "Revert": "Revertir", - "Import from clipboard": "Importar del portapapeles", - "Paste your markdown or webpage here...": "Pega tu markdown o página web aquí...", - "Clear": "Limpiar", - "This note is locked": "Esta nota está bloqueada", - "Sorry, only owner can edit this note.": "Disculpa, solo el dueño puede editar esta nota.", - "OK": "OK", - "Reach the limit": "Haz alcanzado el límite", - "Sorry, you've reached the max length this note can be.": "Disculpa, haz alcanzado la longitud máxima que puede tener esta nota.", - "Please reduce the content or divide it to more notes, thank you!": "Por favor, reduce el contenido o dividela en mas notas, ¡gracias!", - "Import from Gist": "Importar de un Gist", - "Paste your gist url here...": "Pega el URL de tu Gist aquí...", - "Import from Snippet": "Importar de Snippet", - "Select From Available Projects": "Elegir de un proyecto disponible", - "Select From Available Snippets": "Elegir de un Snippet disponible", - "OR": "O", - "Export to Snippet": "Exportar a Snippet", - "Select Visibility Level": "Elegir el nivel de visibilidad", - "Night Theme": "Modo nocturno", - "Follow us on %s and %s.": "Síguenos en %s, y %s.", - "Privacy": "Privacidad", - "Terms of Use": "Términos de uso", - "Do you really want to delete your user account?": "¿Estás seguro que quieres eliminar tu cuenta de usuario?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Esta acción eliminará tu cuenta, todas tus notas y las referencias a tu cuenta desde otras notas.", - "Delete user": "Eliminar usuario", - "Export user data": "Exportar información de usuario", - "Help us translating on %s": "Ayúdanos traduciendo en %s", - "Source Code": "Código fuente", - "Register": "Registrar", - "Powered by %s": "Desarrollado por %s.", - "Help us translating": "Ayúdanos traduciendo", - "Join the community": "Únete a la comunidad", - "Imprint": "Huella" -} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json deleted file mode 100644 index 3dc434ae0..000000000 --- a/locales/fr.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Notes collaboratives en markdown", - "Realtime collaborative markdown notes on all platforms.": "Notes en markdown collaboratives en temps réel sur toutes les plateformes.", - "Best way to write and share your knowledge in markdown.": "Le meilleur moyen d'écrire et partager votre savoir en markdown.", - "Intro": "Intro", - "History": "Historique", - "New guest note": "Nouvelle note invité", - "Collaborate with URL": "Collaborez avec l'URL", - "Support charts and MathJax": "Supporte les graphiques et MathJax", - "Support slide mode": "Supporte le mode présentation", - "Sign In": "Se connecter", - "Below is the history from browser": "Ci-dessous, l'historique du navigateur", - "Welcome!": "Bienvenue !", - "New note": "Nouvelle note", - "or": "ou", - "Sign Out": "Se déconnecter", - "Explore all features": "Explorer toutes les fonctionnalités", - "Select tags...": "Sélectionner les tags...", - "Search keyword...": "Chercher un mot-clef...", - "Sort by title": "Trier par titre", - "Title": "Titre", - "Sort by time": "Trier par date", - "Time": "Date", - "Export history": "Exporter l'historique", - "Import history": "Importer l'historique", - "Clear history": "Supprimer l'historique", - "Refresh history": "Actualiser l'historique", - "No history": "Pas d'historique", - "Import from browser": "Importer depuis le navigateur", - "Releases": "Versions", - "Are you sure?": "Ëtes-vous sûr ?", - "Do you really want to delete this note?": "Voulez-vous vraiment supprimer cette note ?", - "All users will lose their connection.": "Tous les utilisateurs perdront leur connexion.", - "Cancel": "Annuler", - "Yes, do it!": "Oui, je suis sûr !", - "Choose method": "Choisir la méthode", - "Sign in via %s": "Se connecter depuis %s", - "New": "Nouvelle", - "Publish": "Publier", - "Extra": "Extra", - "Revision": "Historique", - "Slide Mode": "Mode présentation", - "Export": "Exporter", - "Import": "Importer", - "Clipboard": "Presse-papier", - "Download": "Télécharger", - "Raw HTML": "HTML brut", - "Edit": "Éditer", - "View": "Voir", - "Both": "Les deux", - "Help": "Aide", - "Upload Image": "Uploader une image", - "Menu": "Menu", - "This page need refresh": "Cette page doit être rechargée", - "You have an incompatible client version.": "Vous avez une version client incompatible.", - "Refresh to update.": "Recharger pour mettre à jour.", - "New version available!": "Nouvelle version disponible !", - "See releases notes here": "Voir les commentaires de version ici", - "Refresh to enjoy new features.": "Recharger pour bénéficier des nouvelles fonctionnalités.", - "Your user state has changed.": "Votre statut utilisateur a changé.", - "Refresh to load new user state.": "Recharger pour avoir le nouveau statut utilisateur.", - "Refresh": "Recharger", - "Contacts": "Contacts", - "Report an issue": "Signaler un problème", - "Meet us on %s": "Rencontrez-nous sur %s", - "Send us email": "Envoyez-nous un mail", - "Documents": "Documents", - "Features": "Fonctionnalités", - "YAML Metadata": "Métadonnées YAML", - "Slide Example": "Exemple de présentation", - "Cheatsheet": "Pense-bête", - "Example": "Exemple", - "Syntax": "Syntaxe", - "Header": "En-tête", - "Unordered List": "Liste à puce", - "Ordered List": "List numérotée", - "Todo List": "Liste de tâches", - "Blockquote": "Citation", - "Bold font": "Gras", - "Italics font": "Italique", - "Strikethrough": "Barré", - "Inserted text": "Souligné", - "Marked text": "Surligné", - "Link": "Lien", - "Image": "Image", - "Code": "Code", - "Externals": "Externes", - "This is a alert area.": "Ceci est un texte d'alerte.", - "Revert": "Revenir en arrière", - "Import from clipboard": "Importer depuis le presse-papier", - "Paste your markdown or webpage here...": "Collez votre markdown ou votre page web ici...", - "Clear": "Vider", - "This note is locked": "Cette note est verrouillée", - "Sorry, only owner can edit this note.": "Désolé, seul le propriétaire peut éditer cette note.", - "OK": "OK", - "Reach the limit": "Atteindre la limite", - "Sorry, you've reached the max length this note can be.": "Désolé, vous avez atteint la longueur maximale que cette note peut avoir.", - "Please reduce the content or divide it to more notes, thank you!": "Merci de réduire le contenu ou de le diviser en plusieurs notes !", - "Import from Gist": "Importer depuis Gist", - "Paste your gist url here...": "Coller l'URL de votre Gist ici...", - "Import from Snippet": "Importer depuis Snippet", - "Select From Available Projects": "Sélectionner depuis les projets disponibles", - "Select From Available Snippets": "Sélectionner depuis les Snippets disponibles", - "OR": "OU", - "Export to Snippet": "Exporter vers Snippet", - "Select Visibility Level": "Sélectionner le niveau de visibilité", - "Night Theme": "Thème Nuit", - "Follow us on %s and %s.": "Suivez-nous sur %s, et %s.", - "Privacy": "Confidentialité", - "Terms of Use": "Conditions d'utilisation", - "Do you really want to delete your user account?": "Voulez-vous vraiment supprimer votre compte utilisateur ?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Cela supprimera votre compte, toutes les notes dont vous êtes propriétaire et supprimera toute référence à votre compte dans les autres notes.", - "Delete user": "Supprimer l'utilisateur", - "Export user data": "Exporter les données utilisateur", - "Help us translating on %s": "Aidez nous à traduire sur %s", - "Source Code": "Code source", - "Register": "S'enregistrer", - "Powered by %s": "Propulsé par %s", - "Help us translating": "Aidez nous à traduire", - "Join the community": "Rejoignez la communauté", - "Imprint": "Mentions légales" -} \ No newline at end of file diff --git a/locales/hi.json b/locales/hi.json deleted file mode 100644 index e8868087b..000000000 --- a/locales/hi.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "सहयोगात्मक मार्कडॉऊन नोट्स", - "Realtime collaborative markdown notes on all platforms.": "सभी प्लेटफार्मों पर वास्तविक समय सहयोगी मार्कडॉऊन के नोट्स", - "Best way to write and share your knowledge in markdown.": "मार्कडॉऊन में लिखने के लिए और अपना ज्ञान शेयर करने का सबसे अच्छा तरीका", - "Intro": "परिचय", - "History": "इतिहास", - "New guest note": "नया अतिथि नोट", - "Collaborate with 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": "सिर्फ एच टी एम एल", - "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": "करने के लिए सूची", - "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...": "यहाँ अपने मार्कडॉऊन या वेब पेज पेस्ट करें ...", - "Clear": "साफ़ करें", - "This note is locked": "इस नोट को बंद कर दिया है", - "Sorry, only owner can edit this note.": "क्षमा करें, केवल मालिक इस नोट को संपादित कर सकते हैं।", - "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 यूआरएल पेस्ट करें ...", - "Import from Snippet": "स्निपेट से आयात करें", - "Select From Available Projects": "उपलब्ध परियोजनाओं से चयन करें", - "Select From Available Snippets": "उपलब्ध स्निपेट से चयन करें", - "OR": "या", - "Export to Snippet": "स्निपेट में निर्यात", - "Select Visibility Level": "दृश्यता के स्तर का चयन" -} diff --git a/locales/hr.json b/locales/hr.json deleted file mode 100644 index e8b4a6fb8..000000000 --- a/locales/hr.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "Kolaborativne markdown bilješke", - "Realtime collaborative markdown notes on all platforms.": "Kolaborativne markdown bilješke na svim platformama u realnom vremenu.", - "Best way to write and share your knowledge in markdown.": "Najbolji način za pisanje i dijeljenje svog znanja u markdown-u.", - "Intro": "Uvod", - "History": "Povijest", - "New guest note": "Nova bilješka gosta", - "Collaborate with URL": "Kolaboracija sa URL-om", - "Support charts and MathJax": "Support charts and MathJax", - "Support slide mode": "Način podrške slajda", - "Sign In": "Prijavu se", - "Below is the history from browser": "Ispod je povijest preglednika", - "Welcome!": "Dobrodošli!", - "New note": "Nova bilješka", - "or": "ili", - "Sign Out": "Odjavi se", - "Explore all features": "Istraži sve značajke", - "Select tags...": "Odaberi oznake...", - "Search keyword...": "Pretraži ključnu riječ...", - "Sort by title": "Sortiraj po naslovu", - "Title": "Naslov", - "Sort by time": "Sortiraj po vremenu", - "Time": "Vrijeme", - "Export history": "Izvezi povijest", - "Import history": "Uvezi povijest", - "Clear history": "Očisti povijest", - "Refresh history": "Osvježi povijest", - "No history": "Nema povijesti", - "Import from browser": "Uvezi iz preglednika", - "Releases": "Izdanja", - "Are you sure?": "Jeste li sigurni?", - "Cancel": "Odustani", - "Yes, do it!": "Da, učini to!", - "Choose method": "Izaberi metodu", - "Sign in via %s": "Prijavi se pomoću %s", - "New": "Novo", - "Publish": "Objavi", - "Extra": "Dodatno", - "Revision": "Revizija", - "Slide Mode": "Način slajda", - "Export": "Izvoz", - "Import": "Uvoz", - "Clipboard": "Međuspremnik", - "Download": "Preuzimanje", - "Raw HTML": "Raw HTML", - "Edit": "Uredi", - "View": "Pregledaj", - "Both": "Oboje", - "Help": "Pomoć", - "Upload Image": "Prenesi sliku", - "Menu": "Meni", - "This page need refresh": "Ovu stranicu je potrebno osvježiti", - "You have an incompatible client version.": "Imate nekompatibilnu verziju klijenta.", - "Refresh to update.": "Osvježite za ažuriranje.", - "New version available!": "Nova verzija dostupna!", - "See releases notes here": "Pogledajte bilješke izdanja ovdje", - "Refresh to enjoy new features.": "Osvježi za nove značajke.", - "Your user state has changed.": "Stanje Vašeg korisnika se promijenilo.", - "Refresh to load new user state.": "Osvježi za učitavanje novog stanja korisnika.", - "Refresh": "Osvježi", - "Contacts": "Kontakti", - "Report an issue": "Prijavi problem", - "Send us email": "Pošalji nam email", - "Documents": "Dokumenti", - "Features": "Značajke", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Primjer slajda", - "Cheatsheet": "Cheatsheet", - "Example": "Primjer", - "Syntax": "Sintaksa", - "Header": "Zaglavlje", - "Unordered List": "Neuređeni popis", - "Ordered List": "Uređeni popis", - "Todo List": "Popis obaveza", - "Blockquote": "Blockquote", - "Bold font": "Bold font", - "Italics font": "Kurzivan font", - "Strikethrough": "Precrtano", - "Inserted text": "Umetnuti tekst", - "Marked text": "Označeni tekst", - "Link": "Link", - "Image": "Slika", - "Code": "Kod", - "Externals": "Vanjski izgled", - "This is a alert area.": "Ovo je područje upozorenja.", - "Revert": "Vrati", - "Import from clipboard": "Uvezi iz međuspremnika", - "Paste your markdown or webpage here...": "Zalijepi svoj markdown ili web stranicu ovdje...", - "Clear": "Očisti", - "This note is locked": "Ova bilješka je zaključana", - "Sorry, only owner can edit this note.": "Žao nam je, samo vlasnik ove bilješke ju može uređivati.", - "OK": "OK", - "Reach the limit": "Dosegni granicu", - "Sorry, you've reached the max length this note can be.": "Žao nam je, dosegli ste maksimalnu moguću duljinu ove bilješke.", - "Please reduce the content or divide it to more notes, thank you!": "Molimo Vas smanjite sardžaj ili ga podijelite na više bilješki, hvala!", - "Import from Gist": "Uvezi iz Gist-a", - "Paste your gist url here...": "Zalijepi svoj gist url ovdje...", - "Import from Snippet": "Uvezi iz isječka", - "Select From Available Projects": "Odaberi iz raspoloživih projekta", - "Select From Available Snippets": "Odaberi iz raspoloživih isječaka", - "OR": "ILI", - "Export to Snippet": "Izvoz u isječak", - "Select Visibility Level": "Odaberi razinu vidljivosti" -} \ No newline at end of file diff --git a/locales/id.json b/locales/id.json deleted file mode 100644 index a6f985a36..000000000 --- a/locales/id.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Catatan markdown kolaboratif", - "Realtime collaborative markdown notes on all platforms.": "Berkolaborasi di catatan markdown di semua platform secara realtime", - "Best way to write and share your knowledge in markdown.": "Platform terbaik untuk menulis dan membagikan markdown", - "Intro": "Perkenalan", - "History": "Riwayat", - "New guest note": "Catatan baru (sebagai tamu)", - "Collaborate with URL": "Kolaborasi real-time", - "Support charts and MathJax": "Mendukung charts dan MathJax", - "Support slide mode": "Mendukung mode slide", - "Sign In": "Masuk", - "Below is the history from browser": "Dibawah ini adalah riwayat dari peramban ini", - "Welcome!": "Selamat Datang", - "New note": "Catatan Baru", - "or": "atau", - "Sign Out": "Keluar", - "Explore all features": "Jelajahi semua fitur", - "Select tags...": "Pilih tanda...", - "Search keyword...": "Cari berdasarkan kata kunci...", - "Sort by title": "Urutkan berdasarkan judul", - "Title": "Judul", - "Sort by time": "Urutkan berdasarkan waktu", - "Time": "Waktu", - "Export history": "Ekspor Riwayat", - "Import history": "Impor Riwayat", - "Clear history": "Bersihkan Riwayat", - "Refresh history": "Muat-ulang Riwayat", - "No history": "Tidak ada riwayat", - "Import from browser": "Impor dari browser", - "Releases": "Penerbitan", - "Are you sure?": "Apakah anda yakin?", - "Do you really want to delete this note?": "Apakah anda yakin ingin menghapus catatan ini?", - "All users will lose their connection.": "Semua pengguna akan kehilangan koneksi nya", - "Cancel": "Batal", - "Yes, do it!": "Ya, lakukan!", - "Choose method": "Pilih cara", - "Sign in via %s": "Masuk menggunakan %s", - "New": "Baru", - "Publish": "Terbitkan", - "Extra": "Tambahan", - "Revision": "Revisi", - "Slide Mode": "Mode Slide", - "Export": "Ekspor", - "Import": "Impor", - "Clipboard": "Papan Klip", - "Download": "Unduh", - "Raw HTML": "File HTML", - "Edit": "Ubah", - "View": "Lihat", - "Both": "Keduanya", - "Help": "Bantuan", - "Upload Image": "Unggah Gambar", - "Menu": "Menu", - "This page need refresh": "Halaman ini perlu dimuat ulang", - "You have an incompatible client version.": "Versi pramban anda tidak kompatibel", - "Refresh to update.": "Muat ulang untuk memperbarui", - "New version available!": "Versi baru tersedia!", - "See releases notes here": "Lihat catatan penerbitan", - "Refresh to enjoy new features.": "Muat ulang untuk menikmati fitur baru.", - "Your user state has changed.": "Data pengguna anda telah berubah.", - "Refresh to load new user state.": "Muat ulang untuk memuat data baru pengguna.", - "Refresh": "Muat ulang", - "Contacts": "Kontak", - "Report an issue": "Laporkan kesalahan", - "Meet us on %s": "Temui kami di %s", - "Send us email": "Kirim kami email", - "Documents": "Dokumen", - "Features": "Fitur", - "YAML Metadata": "Metadata YML", - "Slide Example": "Contoh Slide", - "Cheatsheet": "Cheatsheet", - "Example": "Contoh", - "Syntax": "Sintaks", - "Header": "Header", - "Unordered List": "Daftar tak ber-urutan", - "Ordered List": "Daftar ber-urutan", - "Todo List": "Centang", - "Blockquote": "Blok kutipan", - "Bold font": "Tebal", - "Italics font": "Miring", - "Strikethrough": "Garis", - "Inserted text": "Teks ber-garis bawah", - "Marked text": "Teks yang disorot", - "Link": "Link", - "Image": "Gambar", - "Code": "Kode", - "Externals": "Eksternal", - "This is a alert area.": "Ini adalah area alert.", - "Revert": "Kembalikan", - "Import from clipboard": "Impor dari papan klip", - "Paste your markdown or webpage here...": "Tempel markdown atau halaman web disini", - "Clear": "Bersihkan", - "This note is locked": "Catatan ini terkunci", - "Sorry, only owner can edit this note.": "Maaf, hanya pemilik yang bisa mengubah catatan ini", - "OK": "OK", - "Reach the limit": "Memenuhi batas", - "Sorry, you've reached the max length this note can be.": "Maaf, anda telah memenuhi batas maksimum jumlah catatan ini", - "Please reduce the content or divide it to more notes, thank you!": "Tolong persingkat catatan nya.", - "Import from Gist": "Impor dari Gist", - "Paste your gist url here...": "Templekan URL gist anda disini...", - "Import from Snippet": "Impor dari Snippet", - "Select From Available Projects": "Pilih dari Project yang tersedia", - "Select From Available Snippets": "Pilih dari Snippet yang tersedia", - "OR": "Atau", - "Export to Snippet": "Ekspor ke Snippet", - "Select Visibility Level": "Pilih tingkat penglihatan", - "Night Theme": "Mode Malam", - "Follow us on %s and %s.": "Ikuti kami di %s, dan %s.", - "Privacy": "Privasi", - "Terms of Use": "Aturan Penggunaan", - "Do you really want to delete your user account?": "Apakah anda yakin ingin menghapus akun anda?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Ini akan menghapus akun anda, semua catatan yang dimiliki oleh anda akan dihapus dan menghapus semua referensi ke akun anda dari catatan lain.", - "Delete user": "Hapus pengguna", - "Export user data": "Ekspor data pengguna", - "Help us translating on %s": "Bantu kami menerjemahkan di %s", - "Source Code": "Sumber Kode", - "Register": "Daftar", - "Powered by %s": "Ditenagai oleh %s", - "Help us translating": "Bantu kami menerjemahkan", - "Join the community": "Bergabung dengan komunitas", - "Imprint": "Jejak" -} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json deleted file mode 100644 index d4e20bb38..000000000 --- a/locales/it.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "Entra", - "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": "Entra 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", - "Help us translating on %s": "Aiutaci nella traduzione su %s", - "Source Code": "Codice Sorgente", - "Register": "Registrati", - "Powered by %s": "Alimentato da %s", - "Help us translating": "Aiutaci nella traduzione", - "Join the community": "Unisciti alla comunità", - "Imprint": "Imprint" -} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json deleted file mode 100644 index 0aaf9163d..000000000 --- a/locales/ja.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "ユーザーデータをエクスポート", - "Help us translating on %s": "%s の翻訳にご協力ください", - "Source Code": "ソースコード", - "Register": "登録", - "Powered by %s": "Powered by %s", - "Help us translating": "翻訳のお手伝いをお願いします", - "Join the community": "コミュニティに参加しましょう", - "Imprint": "インプリント" -} \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json deleted file mode 100644 index a1df7b9d6..000000000 --- a/locales/ko.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "Collaborative markdown notes": "협동 마크다운 노트", - "Realtime collaborative markdown notes on all platforms.": "실시간으로 모든 플랫폼에서 마크다운 노트를 함께 작성해보세요.", - "Best way to write and share your knowledge in 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": "체크리스트", - "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...": "마크다운이나 웹페이지 붙여넣기", - "Clear": "Clear", - "This note is locked": "이 노트는 잠겨있습니다.", - "Sorry, only owner can edit this note.": "죄송하지만 소유자만 이 노트를 수정할 수 있습니다.", - "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": "Import from Snippet", - "Select From Available Projects": "가능한 프로젝트 중 선택", - "Select From Available Snippets": "Select From Available Snippets", - "OR": "또는", - "Export to Snippet": "Export to Snippet", - "Select Visibility Level": "Select Visibility Level", - "Night Theme": "다크 테마", - "Follow us on %s and %s.": "%s과 %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", - "Help us translating on %s": "%s에서 번역으로 저희를 도와주세요", - "Source Code": "Source Code" -} \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json deleted file mode 100644 index 250153bbd..000000000 --- a/locales/nl.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Samenwerkende markdown notities", - "Realtime collaborative markdown notes on all platforms.": "Realtime samenwerkende markdown notities.", - "Best way to write and share your knowledge in markdown.": "De beste manier om je kennis vast te leggen en te delen. ", - "Intro": "Introductie", - "History": "Geschiedenis", - "New guest note": "Nieuwe gastnotitie", - "Collaborate with URL": "Samenwerken met URL", - "Support charts and MathJax": "Ondersteunt grafieken en MathJax", - "Support slide mode": "Ondersteunt presentatiemodus", - "Sign In": "Inloggen", - "Below is the history from browser": "Hier onder staat de browser geschiedenis", - "Welcome!": "Welkom!", - "New note": "Nieuwe notitie", - "or": "of", - "Sign Out": "Uitloggen", - "Explore all features": "Ontdek alle features", - "Select tags...": "Selecteer tags...", - "Search keyword...": "Zoeken op keyword...", - "Sort by title": "Sorteren op titel", - "Title": "Titel", - "Sort by time": "Sorteren op tijd", - "Time": "Tijd", - "Export history": "Exporteer geschiedenis", - "Import history": "Importeer geschiedenis", - "Clear history": "Verwijder geschiedenis", - "Refresh history": "Ververs geschiedenis", - "No history": "Geen geschidenis gevonden", - "Import from browser": "Importeer van browser", - "Releases": "Versies", - "Are you sure?": "Weet je het zeker?", - "Do you really want to delete this note?": "Will je deze notitie echt verwijderen?", - "All users will lose their connection.": "Alle gebruikers zullen hun verbinding verliezen.", - "Cancel": "Stoppen", - "Yes, do it!": "Ja, doe het!", - "Choose method": "Kies methode", - "Sign in via %s": "Log in via %s", - "New": "Nieuw", - "Publish": "Publiceren", - "Extra": "Extra", - "Revision": "Versie", - "Slide Mode": "Presentatiemodus", - "Export": "Exporteer", - "Import": "Importeren", - "Clipboard": "Kladbord", - "Download": "Downloaden", - "Raw HTML": "Ruwe HTML", - "Edit": "Aanpassen", - "View": "Bekijken", - "Both": "Beide", - "Help": "Help", - "Upload Image": "Afbeelding uploaden", - "Menu": "Menu", - "This page need refresh": "Deze pagina moet vernieuwd worden", - "You have an incompatible client version.": "Je client is niet compatibel.", - "Refresh to update.": "Ververs om te updaten.", - "New version available!": "Nieuwe versie beschikbaar!", - "See releases notes here": "Bekijk de release notes hier", - "Refresh to enjoy new features.": "Ververs om de nieuwe features te zien.", - "Your user state has changed.": "Je gebruikers-status is veranderd.", - "Refresh to load new user state.": "Ververs om je nieuwe gebruikers-status te zien.", - "Refresh": "Ververs", - "Contacts": "Contacten", - "Report an issue": "Probleem rapporteren", - "Meet us on %s": "Ontmoet ons op %s", - "Send us email": "Stuur ons een mail", - "Documents": "Documenten", - "Features": "Features", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Slide Voorbeeld", - "Cheatsheet": "Spiekbrief", - "Example": "Voorbeeld", - "Syntax": "Syntax", - "Header": "Koptekst", - "Unordered List": "Ongesorteerde Lijst", - "Ordered List": "Gesorteerde List", - "Todo List": "Todo Lijst", - "Blockquote": "Citaat", - "Bold font": "Vette tekst", - "Italics font": "Schuine tekst van maken", - "Strikethrough": "Doorstreepte tekst", - "Inserted text": "Onderstreepte tekst", - "Marked text": "Gemarkeerde tekst", - "Link": "Link", - "Image": "Afbeelding", - "Code": "Code", - "Externals": "Uiterlijkheden", - "This is a alert area.": "Dit is een waarschuwingsgebied.", - "Revert": "Terugzetten", - "Import from clipboard": "Importeren from kladbord", - "Paste your markdown or webpage here...": "Plak je markdown of webpagina hier...", - "Clear": "Legen", - "This note is locked": "Deze notitie is vergrendeld", - "Sorry, only owner can edit this note.": "Sorry, alleen de eigenaar kan deze notitie aanpassen.", - "OK": "OK", - "Reach the limit": "Limiet bereikt", - "Sorry, you've reached the max length this note can be.": "Sorry, je notitie heeft de maximale lengte bereikt.", - "Please reduce the content or divide it to more notes, thank you!": "Verwijder alsjeblieft wat tekst of verdeel het over meerdere notities!", - "Import from Gist": "Importeren vanaf een Gist", - "Paste your gist url here...": "Plak je Gist URL hier...", - "Import from Snippet": "Imporeren vanaf een Snippet", - "Select From Available Projects": "Selecteer van beschikbare projecten", - "Select From Available Snippets": "Selecteer van beschikbare Snippets", - "OR": "OF", - "Export to Snippet": "Exporteren naar Snippet", - "Select Visibility Level": "Selecteer zichtbaarheids niveau", - "Night Theme": "Donkere modus", - "Follow us on %s and %s.": "Volg ons op %s en %s.", - "Privacy": "Privacy", - "Terms of Use": "Gebruikersvoorwaarden", - "Do you really want to delete your user account?": "Weet je zeker dat je je account wilt verwijderen?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Dit zal je account verwijderen. Alle notities waar je eigenaar van bent worden verwijderd, samen met alle verwijzingen naar je account.", - "Delete user": "Gebruiker verwijderen", - "Export user data": "Gebruikersdata exporteren", - "Help us translating on %s": "Help ons vertalen op %s", - "Source Code": "Broncode", - "Register": "Registreren", - "Powered by %s": "Powered by %s", - "Help us translating": "Help ons vertalen", - "Join the community": "Lid worden", - "Imprint": "Afdruk" -} \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json deleted file mode 100644 index 0eeb1b5e3..000000000 --- a/locales/pl.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Wspólne markdown notatki", - "Realtime collaborative markdown notes on all platforms.": "Rzeczywiste wspólne markdown notatki dla wszystkich platform", - "Best way to write and share your knowledge in markdown.": "Najlepszy sposób na pisanie i dzielenie się swoją wiedzą w markdown.", - "Intro": "Intro", - "History": "Historia", - "New guest note": "Nowa notatka gościa", - "Collaborate with URL": "Kolaboracja w czasie rzeczywistym", - "Support charts and MathJax": "Kompatybilne z wykresami oraz MathJax", - "Support slide mode": "Obsługuje tryb slajdów", - "Sign In": "Zaloguj się", - "Below is the history from browser": "Historia z przeglądarki poniżej", - "Welcome!": "Witam!", - "New note": "Nowa notatka", - "or": "lub", - "Sign Out": "Wyloguj się", - "Explore all features": "Przeglądaj wszystkie funkcje", - "Select tags...": "Wybierz tagi...", - "Search keyword...": "Znajdź kluczowe słowo...", - "Sort by title": "Sortuj według tytułu", - "Title": "Tytuł", - "Sort by time": "Sortuj według czasu", - "Time": "Czas", - "Export history": "Eksportuj historię", - "Import history": "Importuj historię", - "Clear history": "Wyczyść historię", - "Refresh history": "Odśwież historię", - "No history": "Brak historii", - "Import from browser": "Importuj z przeglądarki", - "Releases": "Wydania", - "Are you sure?": "Jesteś pewny?", - "Do you really want to delete this note?": "Czy chcesz usunąć tą notatkę?", - "All users will lose their connection.": "Wszyscy użytkownicy stracą swoje połączenie.", - "Cancel": "Anuluj", - "Yes, do it!": "Tak, zrób to!", - "Choose method": "Wybierz metodę", - "Sign in via %s": "Zaloguj się poprzez %s", - "New": "Nowy", - "Publish": "Publikuj", - "Extra": "Ekstra", - "Revision": "Korekta", - "Slide Mode": "Tryb slajdów", - "Export": "Eksport", - "Import": "Import", - "Clipboard": "Schowek", - "Download": "Pobierz", - "Raw HTML": "Raw HTML", - "Edit": "Edytuj", - "View": "Pogląd", - "Both": "Both", - "Help": "Pomoc", - "Upload Image": "Prześlij zdjęcie", - "Menu": "Menu", - "This page need refresh": "Strona wymaga odświeżenia", - "You have an incompatible client version.": "Posiadasz niezgodną wersję kliencką.", - "Refresh to update.": "Odświerz aby zaktualizować.", - "New version available!": "Nowa wersja dostępna!", - "See releases notes here": "Zobacz informacje o wydaniach tutaj", - "Refresh to enjoy new features.": "Odśwież, aby korzystać z nowych funkcji.", - "Your user state has changed.": "Stan twojego użytkownika się zmienił.", - "Refresh to load new user state.": "Odśwież aby załadować nowy stan użytkownika.", - "Refresh": "Odśwież", - "Contacts": "Kontakty", - "Report an issue": "Zgłoś błąd", - "Meet us on %s": "Spotkaj się z nami na %s", - "Send us email": "Wyślij nam email", - "Documents": "Dokumenty", - "Features": "Funkcje", - "YAML Metadata": "YAML Meta dane", - "Slide Example": "Przykład slajdu", - "Cheatsheet": "Ściągawka", - "Example": "Przykład", - "Syntax": "Składnia", - "Header": "Nagłówek", - "Unordered List": "Nie posortowana lista", - "Ordered List": "Posortowana lista", - "Todo List": "Todo lista", - "Blockquote": "Cytat blokowy", - "Bold font": "Czcionka pogrubiona", - "Italics font": "Czcionka pochylona", - "Strikethrough": "Przekreślenie", - "Inserted text": "Wstawiony tekst", - "Marked text": "Zaznaczony tekst", - "Link": "Odnośnik", - "Image": "Zdjęcie", - "Code": "Kod", - "Externals": "Zewnętrzne", - "This is a alert area.": "This is a alert area.", - "Revert": "Cofnij", - "Import from clipboard": "Importuj ze schowka", - "Paste your markdown or webpage here...": "Wklej markdown lub stronę tutaj...", - "Clear": "Wyczyść", - "This note is locked": "Notatka jest zablokowana", - "Sorry, only owner can edit this note.": "Tylko właściciel może edytować tą notatkę.", - "OK": "OK", - "Reach the limit": "Osiągnięto limit", - "Sorry, you've reached the max length this note can be.": "Niestety, osiągnięto maksymalną długość notatki.", - "Please reduce the content or divide it to more notes, thank you!": "Proszę zmniejszyć zawartość notatki lub podzielić ją na kilka notatek, dziękuję!", - "Import from Gist": "Importuj z Gist", - "Paste your gist url here...": "Wklej gist url tutaj...", - "Import from Snippet": "Importuj z Snippet", - "Select From Available Projects": "Wybierz z dostępnych projektów", - "Select From Available Snippets": "Wybierz z dostępnych Snippets", - "OR": "LUB", - "Export to Snippet": "Eksportuj do Snippet", - "Select Visibility Level": "Wybierz poziom widoczności", - "Night Theme": "Motyw Nocny", - "Follow us on %s and %s.": "Znajdź nas na %s oraz %s.", - "Privacy": "Prywatność", - "Terms of Use": "Warunki korzystania", - "Do you really want to delete your user account?": "Czy chcesz usunąć swoje konto użytkownika?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Ta akcja usunie twoje konto, wszystkie notatki które posiadasz oraz wszystkie referencje do tego konta w twoich pozostałych notatkach.", - "Delete user": "Usuń użytkownika", - "Export user data": "Eksportuj dane użytkownika", - "Help us translating on %s": "Pomóż nam przetłumaczyć na język %s", - "Source Code": "Kod źródłowy", - "Register": "Zarejestruj", - "Powered by %s": "Wspierany przez %s", - "Help us translating": "Pomóż nam w tłumaczeniu", - "Join the community": "Dołącz do społeczności", - "Imprint": "Impressum" -} \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json deleted file mode 100644 index 799feea63..000000000 --- a/locales/pt.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Notas em Markdown colaborativas", - "Realtime collaborative markdown notes on all platforms.": "Notas colaborativas em Markdown para todas as plataformas.", - "Best way to write and share your knowledge in markdown.": "A melhor forma de escrever e compartilhar seu conhecimento em Markdown.", - "Intro": "Introdução", - "History": "Histórico", - "New guest note": "Nova nota como convidado", - "Collaborate with URL": "Colaborar via URL", - "Support charts and MathJax": "Suporte para gráficos e MathJax", - "Support slide mode": "Suporte para modo apresentação", - "Sign In": "Entrar", - "Below is the history from browser": "A seguir está o histórico do navegador", - "Welcome!": "Bem vindo!", - "New note": "Nova nota", - "or": "ou", - "Sign Out": "Sair", - "Explore all features": "Explore todas as funções", - "Select tags...": "Selecionar etiquetas...", - "Search keyword...": "Buscar palavra-chave...", - "Sort by title": "Ordenar por título", - "Title": "Título", - "Sort by time": "Ordenar por hora", - "Time": "Hora", - "Export history": "Exportar histórico", - "Import history": "Importar histórico", - "Clear history": "Apagar histórico", - "Refresh history": "Atualizar histórico", - "No history": "Nenhum histórico", - "Import from browser": "Importar do navegador", - "Releases": "Lançamentos", - "Are you sure?": "Tem certeza?", - "Do you really want to delete this note?": "Do you really want to delete this note?", - "All users will lose their connection.": "All users will lose their connection.", - "Cancel": "Cancelar", - "Yes, do it!": "Sim, faça!", - "Choose method": "Escolher método", - "Sign in via %s": "Entrar via %s", - "New": "Novo", - "Publish": "Publicar", - "Extra": "Extra", - "Revision": "Revisão", - "Slide Mode": "Modo Apresentação", - "Export": "Exportar", - "Import": "Importar", - "Clipboard": "Área de transferência", - "Download": "Baixar", - "Raw HTML": "HTML puro", - "Edit": "Editar", - "View": "Ver", - "Both": "Ambos", - "Help": "Ajuda", - "Upload Image": "Carregar Imagem", - "Menu": "Menu", - "This page need refresh": "Esta página precisa ser recarregada", - "You have an incompatible client version.": "Você tem uma versão incompatível do cliente.", - "Refresh to update.": "Recarregar para atualizar.", - "New version available!": "Nova versão disponível!", - "See releases notes here": "Veja notas de lançamento aqui", - "Refresh to enjoy new features.": "Atualize para usar as novas funções.", - "Your user state has changed.": "O estado do seu usuário mudou.", - "Refresh to load new user state.": "Atualize para carregar o novo estado do usuário.", - "Refresh": "Recarregar", - "Contacts": "Contatos", - "Report an issue": "Relatar um problema", - "Meet us on %s": "Meet us on %s", - "Send us email": "Envie-nos um email", - "Documents": "Documentos", - "Features": "Funções", - "YAML Metadata": "Metadados YAML", - "Slide Example": "Exemplo de Apresentação", - "Cheatsheet": "Dicas", - "Example": "Exemplo", - "Syntax": "Sintaxe", - "Header": "Cabeçalho", - "Unordered List": "Lista não ordenada", - "Ordered List": "Lista ordenada", - "Todo List": "Lista de tarefas", - "Blockquote": "Citação", - "Bold font": "Fonte negrito", - "Italics font": "Fonte itálico", - "Strikethrough": "Tachado", - "Inserted text": "Texto inserido", - "Marked text": "Texto marcado", - "Link": "Ligação", - "Image": "Imagem", - "Code": "Código", - "Externals": "Externos", - "This is a alert area.": "Esta é uma área de alerta.", - "Revert": "Reverter", - "Import from clipboard": "Importar da área de transferência", - "Paste your markdown or webpage here...": "Cole seu markdown ou página web aqui...", - "Clear": "Limpar", - "This note is locked": "Esta nota está bloqueada", - "Sorry, only owner can edit this note.": "Desculpe, somente o dono pode editar esta nota.", - "OK": "OK", - "Reach the limit": "Alcançou o limite", - "Sorry, you've reached the max length this note can be.": "Desculpe, você alcançou o tamanho máximo que esta nota pode ter.", - "Please reduce the content or divide it to more notes, thank you!": "Por favor reduza o conteúdo ou divida em mais de uma nota, obrigado!", - "Import from Gist": "Importar de um Gist", - "Paste your gist url here...": "Cole a URL de seu Gist aqui...", - "Import from Snippet": "Importar de Snippet", - "Select From Available Projects": "Selecionar de Projetos Disponíveis", - "Select From Available Snippets": "Selecionar de Snippets Disponíveis", - "OR": "OU", - "Export to Snippet": "Exportar para Snippet", - "Select Visibility Level": "Selecionar Nível de Visibilidade", - "Night Theme": "Night Theme", - "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", - "Help us translating on %s": "Help us translating on %s", - "Source Code": "Source Code", - "Register": "Register", - "Powered by %s": "Powered by %s", - "Help us translating": "Help us translating", - "Join the community": "Join the community", - "Imprint": "Imprint" -} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json deleted file mode 100644 index e11be5a07..000000000 --- a/locales/ru.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "Сотрудничество по ссылке", - "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": "Raw 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": "Список дел", - "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 здесь...", - "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": "Экспортировать данные пользователя", - "Help us translating on %s": "Помогите нам перевести %s", - "Source Code": "Исходный код", - "Register": "Регистрация", - "Powered by %s": "Powered by %s", - "Help us translating": "Помочь с переводом", - "Join the community": "Присоединиться к сообществу", - "Imprint": "Imprint" -} \ No newline at end of file diff --git a/locales/sk.json b/locales/sk.json deleted file mode 100644 index 6c2891a4e..000000000 --- a/locales/sk.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Kolaboratívne markdown poznámky", - "Realtime collaborative markdown notes on all platforms.": "Spolupracujte na markdown poznámkach na všetkých platformách v reálnom čase.", - "Best way to write and share your knowledge in markdown.": "Nejlepšia platforma pre tvorbu a zdieľanie vašich znalostí v markdown.", - "Intro": "Intro", - "History": "História", - "New guest note": "Nová poznámka hosťa", - "Collaborate with URL": "Spolupráca v reálnom čase", - "Support charts and MathJax": "Funguje s grafmi a MathJax", - "Support slide mode": "Podporuje prezentačný režim", - "Sign In": "Prihlásiť sa", - "Below is the history from browser": "Nižšie je história z tohto prehliadača", - "Welcome!": "Vitajte!", - "New note": "Nová poznámka", - "or": "alebo", - "Sign Out": "Odhlásiť sa", - "Explore all features": "Preskúmať všetky funkcie", - "Select tags...": "Zvoliť štítky…", - "Search keyword...": "Vyhľadať klúčové slovo …", - "Sort by title": "Zoradiť podľa názvu", - "Title": "Názov", - "Sort by time": "Zoradiť podľa času", - "Time": "Čas", - "Export history": "Exportovať históriu", - "Import history": "Importovať históriu", - "Clear history": "Odstrániť históriu", - "Refresh history": "Aktualizovať históriu", - "No history": "Žiadna história", - "Import from browser": "Importovať z prehliadača", - "Releases": "Vydania", - "Are you sure?": "Ste si istý?", - "Do you really want to delete this note?": "Naozaj chcete odstrániť túto poznámku?", - "All users will lose their connection.": "Všetci používatelia stratia spojenie.", - "Cancel": "Späť", - "Yes, do it!": "Áno, pokračovať!", - "Choose method": "Zvoliť spôsob", - "Sign in via %s": "Prihlásiť sa cez %s", - "New": "Nová", - "Publish": "Publikovať", - "Extra": "Extra", - "Revision": "Revízia", - "Slide Mode": "Prezentačný režim", - "Export": "Export", - "Import": "Import", - "Clipboard": "Schránka", - "Download": "Stiahnuť", - "Raw HTML": "Raw HTML", - "Edit": "Editovať", - "View": "Zobraziť", - "Both": "Oboje", - "Help": "Pomoc", - "Upload Image": "Nahrať obrázok", - "Menu": "Menu", - "This page need refresh": "Túto stránku je potrebné znovu načítať", - "You have an incompatible client version.": "Verzia vášho klienta nie je kompatibilná.", - "Refresh to update.": "Znovu načítať a aktualizovať", - "New version available!": "Je dostupná nová verzia!", - "See releases notes here": "Pozrite si poznmáky k vydaniu tu", - "Refresh to enjoy new features.": "Znovu načítať a začať si uzívať nové funkcie.", - "Your user state has changed.": "Váš užívateľský stav sa zmenil.", - "Refresh to load new user state.": "Znovu načítať a nahrať užívateľský stav", - "Refresh": "Znovu načítať", - "Contacts": "Kontakty", - "Report an issue": "Nahlásiť problém", - "Meet us on %s": "Stretnite nás na %s", - "Send us email": "Pošlite nám email", - "Documents": "Dokumenty", - "Features": "Funkcie", - "YAML Metadata": "YAML metadáta", - "Slide Example": "Príklad prezentácie", - "Cheatsheet": "Ťahák", - "Example": "Príklad", - "Syntax": "Syntax", - "Header": "Hlavička", - "Unordered List": "Nečíslovaný zoznam", - "Ordered List": "Číslovaný zoznam", - "Todo List": "Kontrolný zoznam", - "Blockquote": "Citácia", - "Bold font": "Tučne", - "Italics font": "Kurzíva", - "Strikethrough": "Preškrtnuté", - "Inserted text": "Vložený text", - "Marked text": "Zvýraznený text", - "Link": "Odkaz", - "Image": "Obrázok", - "Code": "Kód", - "Externals": "Externé", - "This is a alert area.": "Toto je oblasť upozornení.", - "Revert": "Vrátiť", - "Import from clipboard": "Importovať zo schránky", - "Paste your markdown or webpage here...": "Sem vložte váš markdown alebo webovú stránku…", - "Clear": "Vyčistiť", - "This note is locked": "Táto poznámka je zamknutá", - "Sorry, only owner can edit this note.": "Prepáčte, túto poznámku môže editovať iba vlastník.", - "OK": "OK", - "Reach the limit": "Dosiahnutie limitu", - "Sorry, you've reached the max length this note can be.": "Ospravedlňujeme sa, dosiahli ste maximálnu dĺžku poznámky.", - "Please reduce the content or divide it to more notes, thank you!": "Prosím skráťte poznámku.", - "Import from Gist": "Importovať z Gist", - "Paste your gist url here...": "Sem vložte vašu gist url…", - "Import from Snippet": "Importovať zo Snippet", - "Select From Available Projects": "Zvoliť z dostupných projektov", - "Select From Available Snippets": "Zvoliť z dostupných úryvkov", - "OR": "ALEBO", - "Export to Snippet": "Exportovať do úryvku", - "Select Visibility Level": "Zvoliť úroveň viditeľnosti", - "Night Theme": "Nočná téma", - "Follow us on %s and %s.": "Sledujte nás na %s, a %s.", - "Privacy": "Súkromie", - "Terms of Use": "Podmienky použitia", - "Do you really want to delete your user account?": "Naozaj chcete zmazať váš používateľský účet?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Táto operácia odstráni váš účet, všetky poznámky, ktoré vlastníte, a tiež odstráni všetky odkazy na váš účet z iných poznámok.", - "Delete user": "Zmazať používateľa", - "Export user data": "Exportovať používateľské dáta", - "Help us translating on %s": "Help us translating on %s", - "Source Code": "Zdrojový kód", - "Register": "Registrovať", - "Powered by %s": "Powered by %s", - "Help us translating": "Pomôžte nám s prekladom", - "Join the community": "Pripojiť sa ku komunite", - "Imprint": "Odtlačok" -} \ No newline at end of file diff --git a/locales/sr.json b/locales/sr.json deleted file mode 100644 index ef81feffe..000000000 --- a/locales/sr.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "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": "Списак обавеза", - "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": "Увези из Github 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": "Извоз свих корисничких података", - "Help us translating on %s": "Помозите око превода на %s", - "Source Code": "Изворни код", - "Register": "Региструј се", - "Powered by %s": "Покреће %s", - "Help us translating": "Помозите око превода", - "Join the community": "Приступите заједници" -} \ No newline at end of file diff --git a/locales/sv.json b/locales/sv.json deleted file mode 100644 index ef89a27fc..000000000 --- a/locales/sv.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "Collaborative markdown notes": "Kollaborativa markdownanteckningar", - "Realtime collaborative markdown notes on all platforms.": "Kollaborativa markdownantackningar på alla plattformar.", - "Best way to write and share your knowledge in markdown.": "Bästa sättet att skriva och dela din kunskap i markdown.", - "Intro": "Intro", - "History": "Historik", - "New guest note": "Ny gästanteckning", - "Collaborate with URL": "Samarbeta med URL", - "Support charts and MathJax": "Stöd för diagram och MathJax", - "Support slide mode": "Stöd för slide mode", - "Sign In": "Logga in", - "Below is the history from browser": "Nedanför finns historia från webbläsaren", - "Welcome!": "Välkommen!", - "New note": "Ny anteckning", - "or": "eller", - "Sign Out": "Logga ut", - "Explore all features": "Upptäck alla funktioner", - "Select tags...": "Välj taggar...", - "Search keyword...": "Sök nyckelord...", - "Sort by title": "Sortera titlar", - "Title": "Titel", - "Sort by time": "Sortera kronologiskt", - "Time": "Tid", - "Export history": "Exporthistorik", - "Import history": "Importhistorik", - "Clear history": "Rensa historik", - "Refresh history": "Uppdatera historik", - "No history": "Ingen historik", - "Import from browser": "Importera från webbläsare", - "Releases": "Lanseringar", - "Are you sure?": "Är du säker?", - "Do you really want to delete this note?": "Vill du verkligen radera denna anteckning?", - "All users will lose their connection.": "Alla användare kommer att förlora sin anslutning.", - "Cancel": "Avbryt", - "Yes, do it!": "Ja, gör det!", - "Choose method": "Välj metod", - "Sign in via %s": "Logga in via %s", - "New": "Ny", - "Publish": "Publicera", - "Extra": "Extra", - "Revision": "Revision", - "Slide Mode": "Slide Mode", - "Export": "Exportera", - "Import": "Importera", - "Clipboard": "Urklipp", - "Download": "Ladda ner", - "Raw HTML": "Rå HTML", - "Edit": "Redigera", - "View": "Visa", - "Both": "Båda", - "Help": "Hjälp", - "Upload Image": "Ladda upp bilder", - "Menu": "Meny", - "This page need refresh": "Den här sidan behöver laddas om", - "You have an incompatible client version.": "Du har en inkompatibel klientversion.", - "Refresh to update.": "Ladda om för att uppdatera.", - "New version available!": "Ny version tillgänglig!", - "See releases notes here": "Se releaseanteckningar här", - "Refresh to enjoy new features.": "Ladda om för att använda de nya funktionerna.", - "Your user state has changed.": "Din användarstatus har förändrats.", - "Refresh to load new user state.": "Ladda om för att ladda ny användarstatus.", - "Refresh": "Ladda om", - "Contacts": "Kontakter", - "Report an issue": "Rapportera ett fel", - "Meet us on %s": "Träffa oss på %s", - "Send us email": "Skicka e-post till oss", - "Documents": "Dokument", - "Features": "Funktioner", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Slideexempel", - "Cheatsheet": "Cheatsheet", - "Example": "Exempel", - "Syntax": "Syntax", - "Header": "Huvud", - "Unordered List": "Oordnad lists", - "Ordered List": "Ordnad lista", - "Todo List": "Todo-lista", - "Blockquote": "Blockcitat", - "Bold font": "Fet stil", - "Italics font": "Kursiv stil", - "Strikethrough": "Genomstrykning", - "Inserted text": "Insatt text", - "Marked text": "Markerad text", - "Link": "Länk", - "Image": "Bild", - "Code": "Kod", - "Externals": "Externa", - "This is a alert area.": "Det här är ett varnande område.", - "Revert": "Återgå", - "Import from clipboard": "Importera från urklipp", - "Paste your markdown or webpage here...": "Klipp in din markdown eller hemsida här...", - "Clear": "Rensa", - "This note is locked": "Anteckningen är låst", - "Sorry, only owner can edit this note.": "Ursäkta, men endast ägaren kan redigera den här anteckningen.", - "OK": "Okej", - "Reach the limit": "Nå gränsen", - "Sorry, you've reached the max length this note can be.": "Usräkta, men duhar nått maxlängden för vad en anteckning får vara.", - "Please reduce the content or divide it to more notes, thank you!": "Var vänlig förkorta innehållet eller dela upp det i flera anteckningar, tack!", - "Import from Gist": "Importera från Gist", - "Paste your gist url here...": "Klipp in din gist-url här...", - "Import from Snippet": "Importera från Snippet", - "Select From Available Projects": "Välj från tillgängliga projekt", - "Select From Available Snippets": "Välj från tillgängliga Snippets", - "OR": "ELLER", - "Export to Snippet": "Exportera till Snippet", - "Select Visibility Level": "Välj synlighetsnivå", - "Night Theme": "Natttema", - "Follow us on %s and %s.": "Följ oss på %s och %s.", - "Privacy": "Integritet", - "Terms of Use": "Villkor", - "Do you really want to delete your user account?": "Vill du verkligen ta bort ditt användarkonto?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Detta tar bort ditt konto, alla anteckningar som ägs av dig och tar bort alla referenser till ditt konto från andra anteckningar.", - "Delete user": "Ta bort användare", - "Export user data": "Exportera användardata", - "Help us translating on %s": "Hjälp oss att översätta på %s", - "Source Code": "Källkod", - "Register": "Registrera", - "Powered by %s": "Drivs av %s", - "Help us translating": "Hjälp oss att översätta", - "Join the community": "Gå med i samhället", - "Imprint": "Avtryck" -} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json deleted file mode 100644 index b0682469a..000000000 --- a/locales/tr.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "Ortak markdown notları", - "Realtime collaborative markdown notes on all platforms.": "Tüm platformlarda gerçek zamanlı markdown notları", - "Best way to write and share your knowledge in markdown.": "Markdownda bilginizi paylaşmanın en kolay yolu.", - "Intro": "Giriş", - "History": "Geçmiş", - "New guest note": "Yeni kullanıcı notu", - "Collaborate with URL": "URL ile katkıda bulun", - "Support charts and MathJax": "Grafikler ve MathJax'ı destekle", - "Support slide mode": "Sunum modunu destekle", - "Sign In": "Kaydol", - "Below is the history from browser": "Aşağıda tarayıcınızın geçmişi var", - "Welcome!": "Hoşgeldiniz!", - "New note": "Yeni not", - "or": "veya", - "Sign Out": "Çıkış Yap", - "Explore all features": "Özellikleri keşfet", - "Select tags...": "Etiketleri seçin...", - "Search keyword...": "Anahtar kelimeleri arayın...", - "Sort by title": "Başlığa göre sırala", - "Title": "Başlık", - "Sort by time": "Zamana göre sırala", - "Time": "Zaman", - "Export history": "Geçmişe dışa aktar", - "Import history": "Geçmişi içe aktar", - "Clear history": "Geçmişi temizle", - "Refresh history": "Geçmişi yenile", - "No history": "Geçmiş yok", - "Import from browser": "Tarayıcıdan içe aktar", - "Releases": "Sürümler", - "Are you sure?": "Emin misiniz?", - "Cancel": "İptal", - "Yes, do it!": "Evet, devam et!", - "Choose method": "Metot seçin", - "Sign in via %s": "%s ile giriş yapın", - "New": "Yeni", - "Publish": "Yayınla", - "Extra": "Ekstra", - "Revision": "Sürüm", - "Slide Mode": "Slayt Modu", - "Export": "Dışa Aktar", - "Import": "İçe Aktar", - "Clipboard": "Pano", - "Download": "İndir", - "Raw HTML": "Kaynak HTML", - "Edit": "Düzenle", - "View": "İncele", - "Both": "İkisi de", - "Help": "Yardım", - "Upload Image": "Resim Yükle", - "Menu": "Menü", - "This page need refresh": "Bu sayfayı yeniden yüklemek lazım", - "You have an incompatible client version.": "Yerel uygulamanız uyumlu olmayan bir sürümde.", - "Refresh to update.": "Güncellemek için yenileyin.", - "New version available!": "Yeni versiyon kullanıma hazır!", - "See releases notes here": "Değişikliklere burdan bakabilirsiniz", - "Refresh to enjoy new features.": "Yeni özelliklere yenileyerek erişebilirsiniz.", - "Your user state has changed.": "Kullanıcı durumunuz değişti.", - "Refresh to load new user state.": "Yeni kullanıcı durumunuzu yüklemek için yenileyin.", - "Refresh": "Yenile", - "Contacts": "Kişiler", - "Report an issue": "Hata bildir", - "Send us email": "Bize email gönderin", - "Documents": "Belgeler", - "Features": "Özellikler", - "YAML Metadata": "YAML Özbilgisi", - "Slide Example": "Slayt Örneği", - "Cheatsheet": "Cheatsheet", - "Example": "Örnek", - "Syntax": "Sentaks", - "Header": "Başlık", - "Unordered List": "Sırasız Liste", - "Ordered List": "Sıralı Liste", - "Todo List": "Yapılacaklar Listesi", - "Blockquote": "Alıntı Bloğu", - "Bold font": "Kalın yazı", - "Italics font": "İtalik yazı", - "Strikethrough": "Üstüçizili", - "Inserted text": "Eklenmiş yazı", - "Marked text": "Seçili yazı", - "Link": "Link", - "Image": "Resim", - "Code": "Kod", - "Externals": "Dış veriler", - "This is a alert area.": "Burası bir uyarı bölgesi.", - "Revert": "Geri al", - "Import from clipboard": "Panodan içe aktar", - "Paste your markdown or webpage here...": "Markdown veya web sayfanızı buraya yapıştırın...", - "Clear": "Temizle", - "This note is locked": "Burası kilitli", - "Sorry, only owner can edit this note.": "Üzgünüm, bu not sadece sahibi tarafından düzenlenebilir.", - "OK": "Tamam", - "Reach the limit": "Limite eriş", - "Sorry, you've reached the max length this note can be.": "Üzgünüm, bu not için maksimum harf sayısına ulaştınız", - "Please reduce the content or divide it to more notes, thank you!": "Lütfen notu değiştirin veya birden fazla notlara bölün, teşekkürler!", - "Import from Gist": "Gist'ten içe aktar", - "Paste your gist url here...": "Gist URL'nizi buraya yapıştırın...", - "Import from Snippet": "Snippet'ten içe aktar", - "Select From Available Projects": "Uygun Projelerden Seçin", - "Select From Available Snippets": "Uygun Snippet'lerden Seçin", - "OR": "VEYA", - "Export to Snippet": "Snippet olarak dışa aktarın", - "Select Visibility Level": "Görünebilirlik seviyesini belirleyin" -} diff --git a/locales/uk.json b/locales/uk.json deleted file mode 100644 index 0afc8796b..000000000 --- a/locales/uk.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Collaborative markdown notes": "Спільні примітки щодо знижок", - "Realtime collaborative markdown notes on all platforms.": "Спільні примітки щодо знижок в реальному часі на всіх платформах.", - "Best way to write and share your knowledge in 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": "Raw 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": "Список завдань", - "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 тут...", - "Import from Snippet": "Імпортувати фрагмент коду", - "Select From Available Projects": "Виберіть з доступних проектів", - "Select From Available Snippets": "Виберіть з доступних фрагментів коду", - "OR": "АБО", - "Export to Snippet": "Експорт фрагменту коду", - "Select Visibility Level": "Вибрати рівень видимості" -} diff --git a/locales/vi.json b/locales/vi.json deleted file mode 100644 index 40c9257a0..000000000 --- a/locales/vi.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "Collaborative markdown notes": "Cộng tác ghi chú markdown", - "Realtime collaborative markdown notes on all platforms.": "Cộng tác ghi chú markdown đa nền tảng thời gian thực", - "Best way to write and share your knowledge in markdown.": "Nền tảng tốt nhất để viết và chia sẻ markdown", - "Intro": "Giới thiệu", - "History": "Lịch sử", - "New guest note": "Khách mới", - "Collaborate with URL": "Cộng tác thời gian thực", - "Support charts and MathJax": "Làm việc với biểu đồ và MathJax", - "Support slide mode": "Hỗ trợ chế độ slide", - "Sign In": "Đăng nhập", - "Below is the history from browser": "Dưới đây là lịch sử của trình duyệt", - "Welcome!": "Chào mừng bạn!", - "New note": "Tạo mới ghi chú", - "or": "hoặc", - "Sign Out": "Đăng xuất", - "Explore all features": "Khám phá tất cả tính năng", - "Select tags...": "Chọn tag", - "Search keyword...": "Tìm kiếm", - "Sort by title": "Sắp xếp theo tiêu đề", - "Title": "Tiêu đề", - "Sort by time": "Sắp xếp theo thời gian", - "Time": "Thời gian", - "Export history": "Xuất lịch sử", - "Import history": "Nhập lịch sử", - "Clear history": "Xóa lịch sử", - "Refresh history": "Làm mới lịch sử", - "No history": "Không có lịch sử", - "Import from browser": "Nhập từ trình duyệt", - "Releases": "Xuất bản", - "Are you sure?": "Bạn có chắc chắn không ?", - "Do you really want to delete this note?": "Bạn có thực sự muốn xóa ghi chú này ?", - "All users will lose their connection.": "Tất cả người dùng sẽ mất liên kết này.", - "Cancel": "Hủy", - "Yes, do it!": "Đồng ý", - "Choose method": "Chọn phương thức", - "Sign in via %s": "Đăng nhấp với %s", - "New": "Mới", - "Publish": "Xuất bản", - "Extra": "Extra", - "Revision": "Sửa đổi", - "Slide Mode": "Chế độ slide", - "Export": "Xuất", - "Import": "Nhập", - "Clipboard": "Clipboard", - "Download": "Tải xuống", - "Raw HTML": "Raw HTML", - "Edit": "Sửa", - "View": "Hiện", - "Both": "Cả hai", - "Help": "Trợ giúp", - "Upload Image": "Tải ảnh lên", - "Menu": "Menu", - "This page need refresh": "Trang này cần được làm mới", - "You have an incompatible client version.": "Phiên bản của client không tương thích.", - "Refresh to update.": "Làm mới để cập nhập.", - "New version available!": "Phiên bản mới đã có sẵn.", - "See releases notes here": "Xem ghi chú xuất bản ở đây.", - "Refresh to enjoy new features.": "Làm mới để trải nghiệm tính năng mới.", - "Your user state has changed.": "Trạng thái người dùng bị thay đổi.", - "Refresh to load new user state.": "Làm mới để cập nhập trạng thái người dùng mới.", - "Refresh": "Làm mới", - "Contacts": "Liên Lạc", - "Report an issue": "Báo cáo vấn đề", - "Meet us on %s": "Gặp chúng tôi ở %s", - "Send us email": "Gửi email cho chúng tôi", - "Documents": "Tài liệu", - "Features": "Tính năng", - "YAML Metadata": "YAML Metadata", - "Slide Example": "Slide ví dụ", - "Cheatsheet": "Cheetsheet", - "Example": "Ví dụ", - "Syntax": "Cú pháp", - "Header": "Đầu đề", - "Unordered List": "Danh sách chưa sắp xếp", - "Ordered List": "Danh sách đã sắp xếp", - "Todo List": "Checklist", - "Blockquote": "Blockquote", - "Bold font": "Bôi đậm", - "Italics font": "In nghiêng", - "Strikethrough": "Gạch ngang", - "Inserted text": "Gạch chân", - "Marked text": "Hightlight", - "Link": "Liên kết", - "Image": "Ảnh", - "Code": "Code", - "Externals": "Externals", - "This is a alert area.": "Đây là khu vực cảnh báo", - "Revert": "Trở lại như cũ", - "Import from clipboard": "Thêm từ clipboard", - "Paste your markdown or webpage here...": "Dán markdown hoặc webpage ở đây ...", - "Clear": "Xóa", - "This note is locked": "Ghi chú này bị khóa.", - "Sorry, only owner can edit this note.": "Xin lỗi, chỉ chủ sở hữu có thể xóa note.", - "OK": "Đồng ý", - "Reach the limit": "Đạt giới hạn", - "Sorry, you've reached the max length this note can be.": "Rất tiếc, bạn đã đạt tới độ dài tối đa ", - "Please reduce the content or divide it to more notes, thank you!": "Vui lòng rút ngắn ghi chú", - "Import from Gist": "Nhập từ Gist", - "Paste your gist url here...": "Dán liên kết gist vào đây ...", - "Import from Snippet": "Thêm từ Snippet", - "Select From Available Projects": "Chọn từ Project có sẵn", - "Select From Available Snippets": "Chọn từ Snippets có sẵn", - "OR": "HOẶC", - "Export to Snippet": "Xuất ra Snippet", - "Select Visibility Level": "Chọn cấp độ hiển thị", - "Night Theme": "Giao diện tối", - "Follow us on %s and %s.": "Cho phép chúng tôi %s, và %s.", - "Privacy": "Quyền riêng tư.", - "Terms of Use": "Điều khoản sử dụng.", - "Do you really want to delete your user account?": "Bạn có thực sự muốn xóa tài khoản ?", - "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "Điều này sẽ xóa tài khoản của bạn, tất cả các ghi chú thuộc sở hữu của bạn và xóa tất cả các liên kết đến tài khoản của bạn khỏi các ghi chú khác.", - "Delete user": "Xóa người dùng", - "Export user data": "Xuất dữ liệu người dùng", - "Help us translating on %s": "Giúp chúng tôi dịch trên %s", - "Source Code": "Mã nguồn", - "Register": "Đăng ký", - "Powered by %s": "Cung cấp bởi %s", - "Help us translating": "Giúp chúng tôi dịch", - "Join the community": "Tham gia vào cộng đồng" -} \ No newline at end of file diff --git a/locales/zh-CN.json b/locales/zh-CN.json deleted file mode 100644 index deabf0e1c..000000000 --- a/locales/zh-CN.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "实时协作", - "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": "清单", - "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": "好的", - "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 网址...", - "Import from Snippet": "从 Snippet 导入", - "Select From Available Projects": "从可用的项目中选择", - "Select From Available Snippets": "从可用的 Snippet 中选择", - "OR": "或", - "Export to Snippet": "导出到 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": "导出用户数据", - "Help us translating on %s": "在 %s 上帮我们翻译", - "Source Code": "源代码", - "Register": "注册", - "Powered by %s": "由 %s 驱动", - "Help us translating": "帮助我们翻译", - "Join the community": "加入社区", - "Imprint": "法律声明" -} \ No newline at end of file diff --git a/locales/zh-TW.json b/locales/zh-TW.json deleted file mode 100644 index 2e674f90f..000000000 --- a/locales/zh-TW.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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": "使用網址協作", - "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 Metadata", - "Slide Example": "簡報範例", - "Cheatsheet": "快速簡表", - "Example": "範例", - "Syntax": "語法", - "Header": "標題", - "Unordered List": "無序清單", - "Ordered List": "有序清單", - "Todo List": "待辦事項", - "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": "好的", - "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 網址...", - "Import from Snippet": "從 Snippet 匯入", - "Select From Available Projects": "從可用的專案中選擇", - "Select From Available Snippets": "從可用的 Snippets 中選擇", - "OR": "或是", - "Export to Snippet": "匯出到 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": "匯出使用者資料", - "Help us translating on %s": "來 %s 幫我們翻譯", - "Source Code": "原始碼", - "Register": "註冊", - "Powered by %s": "由 %s 強力驅動", - "Help us translating": "幫助我們改進翻譯", - "Join the community": "加入社群", - "Imprint": "版本說明" -} \ No newline at end of file diff --git a/old_src/lib/app.ts b/old_src/lib/app.ts deleted file mode 100644 index aa67b73f2..000000000 --- a/old_src/lib/app.ts +++ /dev/null @@ -1,312 +0,0 @@ -import compression from 'compression' -import flash from 'connect-flash' -// eslint-disable-next-line @typescript-eslint/camelcase -import connect_session_sequelize from 'connect-session-sequelize' -import cookieParser from 'cookie-parser' -import ejs from 'ejs' -import express from 'express' -import session from 'express-session' -import childProcess from 'child_process' -import helmet from 'helmet' -import http from 'http' -import https from 'https' -import i18n from 'i18n' -import fs from 'fs' -import methodOverride from 'method-override' -import morgan from 'morgan' -import passport from 'passport' -import passportSocketIo from 'passport.socketio' -import path from 'path' -import SocketIO from 'socket.io' -import WebSocket from 'ws' - -import { config } from './config' -import { addNonceToLocals, computeDirectives } from './csp' -import { errors } from './errors' -import { logger } from './logger' -import { Revision, sequelize, runMigrations } from './models' -import { realtime, State } from './realtime' -import { handleTermSignals } from './utils/functions' -import { AuthRouter, BaseRouter, HistoryRouter, ImageRouter, NoteRouter, StatusRouter, UserRouter } from './web' -import { tooBusy, checkURI, redirectWithoutTrailingSlashes, codiMDVersion } from './web/middleware' - -const rootPath = path.join(__dirname, '..') - -// session store -const SequelizeStore = connect_session_sequelize(session.Store) - -const sessionStore = new SequelizeStore({ - db: sequelize -}) - -// server setup -const app = express() -let server: http.Server -if (config.useSSL) { - const ca: string[] = [] - for (const path of config.sslCAPath) { - ca.push(fs.readFileSync(path, 'utf8')) - } - - const options = { - key: fs.readFileSync(config.sslKeyPath, 'utf8'), - cert: fs.readFileSync(config.sslCertPath, 'utf8'), - ca: ca, - dhparam: fs.readFileSync(config.dhParamPath, 'utf8'), - requestCert: false, - rejectUnauthorized: false, - heartbeatInterval: config.heartbeatInterval, - heartbeatTimeout: config.heartbeatTimeout - } - server = https.createServer(options, app) -} else { - server = http.createServer(app) -} - -// if we manage to provide HTTPS domains, but don't provide TLS ourselves -// obviously a proxy is involded. In order to make sure express is aware of -// this, we provide the option to trust proxies here. -if (!config.useSSL && config.protocolUseSSL) { - app.set('trust proxy', 1) -} - -// socket io -const io = SocketIO(server, { cookie: false }) -io.engine.ws = new WebSocket.Server({ - noServer: true, - perMessageDeflate: false -}) -// assign socket io to realtime -realtime.io = io - -// socket.io secure -io.use(realtime.secure) -// socket.io auth -io.use(passportSocketIo.authorize({ - cookieParser: cookieParser, - key: config.sessionName, - secret: config.sessionSecret, - store: sessionStore, - success: realtime.onAuthorizeSuccess, - fail: realtime.onAuthorizeFail -})) -// socket.io connection -io.sockets.on('connection', realtime.connection) - -// logger -app.use(morgan('combined', { - stream: { - write: function (message): void { - logger.info(message) - } - } -})) - -// use hsts to tell https users stick to this -if (config.hsts.enable) { - app.use(helmet.hsts({ - maxAge: config.hsts.maxAgeSeconds, - includeSubdomains: config.hsts.includeSubdomains, - preload: config.hsts.preload - })) -} else if (config.useSSL) { - logger.info('Consider enabling HSTS for extra security:') - logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security') -} - -// Generate a random nonce per request, for CSP with inline scripts -app.use(addNonceToLocals) - -// use Content-Security-Policy to limit XSS, dangerous plugins, etc. -// https://helmetjs.github.io/docs/csp/ -if (config.csp.enable) { - app.use(helmet.contentSecurityPolicy({ - directives: computeDirectives() - })) -} else { - logger.info('Content-Security-Policy is disabled. This may be a security risk.') -} - -// Add referrer policy to improve privacy -app.use( - helmet.referrerPolicy({ - policy: 'same-origin' - }) -) - -// methodOverride -app.use(methodOverride('_method')) - -// compression -app.use(compression()) - -app.use(cookieParser()) - -i18n.configure({ - locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr', 'vi', 'ar', 'cs', 'sk'], - cookie: 'locale', - indent: ' ', // this is the style poeditor.com exports it, this creates less churn - directory: path.resolve(rootPath, config.localesPath), - updateFiles: config.updateI18nFiles -}) - -app.use(i18n.init) - -// set generally available variables for all views -app.locals.useCDN = config.useCDN -app.locals.serverURL = config.serverURL -app.locals.sourceURL = config.sourceURL -app.locals.allowAnonymous = config.allowAnonymous -app.locals.allowAnonymousEdits = config.allowAnonymousEdits -app.locals.authProviders = { - facebook: config.isFacebookEnable, - twitter: config.isTwitterEnable, - github: config.isGitHubEnable, - gitlab: config.isGitLabEnable, - dropbox: config.isDropboxEnable, - google: config.isGoogleEnable, - ldap: config.isLDAPEnable, - ldapProviderName: config.ldap.providerName, - saml: config.isSAMLEnable, - oauth2: config.isOAuth2Enable, - oauth2ProviderName: config.oauth2.providerName, - openID: config.isOpenIDEnable, - email: config.isEmailEnable, - allowEmailRegister: config.allowEmailRegister -} - -// Export/Import menu items -app.locals.enableDropBoxSave = config.isDropboxEnable -app.locals.enableGitHubGist = config.isGitHubEnable -app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable - -// session -app.use(session({ - name: config.sessionName, - secret: config.sessionSecret, - resave: false, // don't save session if unmodified - saveUninitialized: true, // always create session to ensure the origin - rolling: true, // reset maxAge on every response - cookie: { - maxAge: config.sessionLife, - sameSite: 'lax', - secure: config.useSSL || config.protocolUseSSL || false - }, - store: sessionStore -})) - -// session resumption -const tlsSessionStore = {} -server.on('newSession', function (id, data, cb) { - tlsSessionStore[id.toString('hex')] = data - cb() -}) -server.on('resumeSession', function (id, cb) { - cb(null, tlsSessionStore[id.toString('hex')] || null) -}) - -// middleware which blocks requests when we're too busy -app.use(tooBusy) - -app.use(flash()) - -// passport -app.use(passport.initialize()) -app.use(passport.session()) - -// check uri is valid before going further -app.use(checkURI) -// redirect url without trailing slashes -app.use(redirectWithoutTrailingSlashes) -app.use(codiMDVersion) - -// routes without sessions -// static files -app.use('/', express.static(path.resolve(rootPath, config.publicPath), { maxAge: config.staticCacheTime, index: false, redirect: false })) -app.use('/docs', express.static(path.resolve(rootPath, config.docsPath), { maxAge: config.staticCacheTime, redirect: false })) -app.use('/uploads', express.static(path.resolve(rootPath, config.uploadsPath), { maxAge: config.staticCacheTime, redirect: false })) -app.use('/default.md', express.static(path.resolve(rootPath, config.defaultNotePath), { maxAge: config.staticCacheTime })) - -// routes need sessions -// template files -app.set('views', config.viewPath) -// set render engine -app.engine('ejs', ejs.renderFile) -// set view engine -app.set('view engine', 'ejs') - -app.use(BaseRouter) -app.use(StatusRouter) -app.use(AuthRouter) -app.use(HistoryRouter) -app.use(UserRouter) -app.use(ImageRouter) -app.use(NoteRouter) - -// response not found if no any route matxches -app.get('*', function (req, res) { - errors.errorNotFound(res) -}) - -// log uncaught exception -process.on('uncaughtException', function (err) { - logger.error('An uncaught exception has occured.') - logger.error(err) - logger.error('Process will exit now.') - process.exit(1) -}) - -// listen -function startListen (): void { - let address - const listenCallback = function (): void { - const schema = config.useSSL ? 'HTTPS' : 'HTTP' - logger.info('%s Server listening at %s', schema, address) - realtime.state = State.Running - } - - const unixCallback = function (): void { - const throwErr = function (err): void { if (err) throw err } - if (config.socket.owner !== undefined) { - childProcess.spawn('chown', [config.socket.owner, config.path]).on('error', throwErr) - } - if (config.socket.group !== undefined) { - childProcess.spawn('chgrp', [config.socket.group, config.path]).on('error', throwErr) - } - if (config.socket.mode !== undefined) { - fs.chmod(config.path, config.socket.mode, throwErr) - } - listenCallback() - } - // use unix domain socket if 'path' is specified - if (config.path) { - address = config.path - server.listen(config.path, unixCallback) - } else { - address = config.host + ':' + config.port - server.listen(config.port, config.host, listenCallback) - } -} - -// sync db then start listen -sequelize.authenticate().then(async function () { - await runMigrations() - sessionStore.sync() - // check if realtime is ready - if (realtime.isReady()) { - Revision.checkAllNotesRevision(function (err, notes) { - if (err) { - throw new Error(err) - } - if (!notes || notes.length <= 0) { - return startListen() - } - }) - } else { - throw new Error('server still not ready after db synced') - } -}) - -process.on('SIGINT', () => handleTermSignals(io)) -process.on('SIGTERM', () => handleTermSignals(io)) -process.on('SIGQUIT', () => handleTermSignals(io)) diff --git a/old_src/lib/config/default.ts b/old_src/lib/config/default.ts deleted file mode 100644 index 63a8249d9..000000000 --- a/old_src/lib/config/default.ts +++ /dev/null @@ -1,122 +0,0 @@ -import os from 'os' -import { Config } from './interfaces' -import { Permission } from './enum' - -export const defaultConfig: Config = { - permission: Permission, - domain: '', - urlPath: '', - host: '0.0.0.0', - port: 3000, - socket: { - group: undefined, - owner: undefined, - mode: undefined - }, - loglevel: 'info', - urlAddPort: false, - allowOrigin: ['localhost'], - useSSL: false, - hsts: { - enable: true, - maxAgeSeconds: 60 * 60 * 24 * 365, - includeSubdomains: true, - preload: true - }, - csp: { - enable: true, - directives: {}, - addDefaults: true, - addDisqus: true, - addGoogleAnalytics: true, - upgradeInsecureRequests: 'auto', - reportURI: undefined - }, - protocolUseSSL: false, - useCDN: false, - allowAnonymous: true, - allowAnonymousEdits: false, - allowFreeURL: false, - forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'], - defaultPermission: 'editable', - dbURL: '', - db: {}, - // ssl path - sslKeyPath: '', - sslCertPath: '', - sslCAPath: [], - dhParamPath: '', - // other path - publicPath: './public', - viewPath: './public/views', - tmpPath: os.tmpdir(), - defaultNotePath: './public/default.md', - docsPath: './public/docs', - uploadsPath: './public/uploads', - localesPath: './locales', - // session - sessionName: 'connect.sid', - sessionSecret: 'secret', - sessionSecretLen: 128, - sessionLife: 14 * 24 * 60 * 60 * 1000, // 14 days - staticCacheTime: 1 * 24 * 60 * 60 * 1000, // 1 day - // socket.io - heartbeatInterval: 5000, - heartbeatTimeout: 10000, - // too busy timeout - tooBusyLag: 70, - // document - documentMaxLength: 100000, - // image upload setting, available options are imgur/s3/filesystem/azure/lutim - imageUploadType: 'filesystem', - lutim: { - url: 'https://framapic.org/' - }, - minio: { - accessKey: undefined, - secretKey: undefined, - endPoint: undefined, - secure: true, - port: 9000 - }, - gitlab: { - baseURL: undefined, - clientID: undefined, - clientSecret: undefined, - scope: undefined, - version: 'v4' - }, - saml: { - idpSsoUrl: undefined, - idpCert: undefined, - issuer: undefined, - identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - disableRequestedAuthnContext: false, - groupAttribute: undefined, - externalGroups: [], - requiredGroups: [], - attribute: { - id: undefined, - username: undefined, - email: undefined - } - }, - email: true, - allowEmailRegister: true, - allowGravatar: true, - openID: false, - // linkifyHeaderStyle - How is a header text converted into a link id. - // Header Example: "3.1. Good Morning my Friend! - Do you have 5$?" - // * 'keep-case' is the legacy CodiMD value. - // Generated id: "31-Good-Morning-my-Friend---Do-you-have-5" - // * 'lower-case' is the same like legacy (see above), but converted to lower-case. - // Generated id: "#31-good-morning-my-friend---do-you-have-5" - // * 'gfm' _GitHub-Flavored Markdown_ style as described here: - // https://gist.github.com/asabaylus/3071099#gistcomment-1593627 - // It works like 'lower-case', but making sure the ID is unique. - // This is What GitHub, GitLab and (hopefully) most other tools use. - // Generated id: "31-good-morning-my-friend---do-you-have-5" - // 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1" - // 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2" - linkifyHeaderStyle: 'keep-case' -} diff --git a/old_src/lib/config/defaultSSL.ts b/old_src/lib/config/defaultSSL.ts deleted file mode 100644 index 79b27674f..000000000 --- a/old_src/lib/config/defaultSSL.ts +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'fs' - -function getFile (path): string { - if (fs.existsSync(path)) { - return path - } - return '' -} - -export const defaultSSL = { - sslKeyPath: getFile('/run/secrets/key.pem'), - sslCertPath: getFile('/run/secrets/cert.pem'), - sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], - dhParamPath: getFile('/run/secrets/dhparam.pem') -} diff --git a/old_src/lib/config/dockerSecret.ts b/old_src/lib/config/dockerSecret.ts deleted file mode 100644 index 51b232b85..000000000 --- a/old_src/lib/config/dockerSecret.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const basePath = path.resolve('/run/secrets/') - -function getSecret (secret): string | undefined { - const filePath = path.join(basePath, secret) - if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf-8') - return undefined -} - -export let dockerSecret: { s3: { accessKeyId: string | undefined; secretAccessKey: string | undefined }; github: { clientID: string | undefined; clientSecret: string | undefined }; facebook: { clientID: string | undefined; clientSecret: string | undefined }; google: { clientID: string | undefined; hostedDomain: string | undefined; clientSecret: string | undefined }; sessionSecret: string | undefined; sslKeyPath: string | undefined; twitter: { consumerSecret: string | undefined; consumerKey: string | undefined }; dropbox: { clientID: string | undefined; clientSecret: string | undefined; appKey: string | undefined }; gitlab: { clientID: string | undefined; clientSecret: string | undefined }; imgur: string | undefined; sslCertPath: string | undefined; sslCAPath: (string | undefined)[]; dhParamPath: string | undefined; dbURL: string | undefined; azure: { connectionString: string | undefined } } - -if (fs.existsSync(basePath)) { - dockerSecret = { - dbURL: getSecret('dbURL'), - sessionSecret: getSecret('sessionsecret'), - sslKeyPath: getSecret('sslkeypath'), - sslCertPath: getSecret('sslcertpath'), - sslCAPath: [getSecret('sslcapath')], - dhParamPath: getSecret('dhparampath'), - s3: { - accessKeyId: getSecret('s3_acccessKeyId'), - secretAccessKey: getSecret('s3_secretAccessKey') - }, - azure: { - connectionString: getSecret('azure_connectionString') - }, - facebook: { - clientID: getSecret('facebook_clientID'), - clientSecret: getSecret('facebook_clientSecret') - }, - twitter: { - consumerKey: getSecret('twitter_consumerKey'), - consumerSecret: getSecret('twitter_consumerSecret') - }, - github: { - clientID: getSecret('github_clientID'), - clientSecret: getSecret('github_clientSecret') - }, - gitlab: { - clientID: getSecret('gitlab_clientID'), - clientSecret: getSecret('gitlab_clientSecret') - }, - dropbox: { - clientID: getSecret('dropbox_clientID'), - clientSecret: getSecret('dropbox_clientSecret'), - appKey: getSecret('dropbox_appKey') - }, - google: { - clientID: getSecret('google_clientID'), - clientSecret: getSecret('google_clientSecret'), - hostedDomain: getSecret('google_hostedDomain') - }, - imgur: getSecret('imgur_clientid') - } -} diff --git a/old_src/lib/config/enum.ts b/old_src/lib/config/enum.ts deleted file mode 100644 index 2ebb1a275..000000000 --- a/old_src/lib/config/enum.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Environment { - development: string; - production: string; - test: string; -} - -export const Environment: Environment = { - development: 'development', - production: 'production', - test: 'test' -} - -export interface Permission { - freely: string; - editable: string; - limited: string; - locked: string; - protected: string; - private: string; -} - -export const Permission: Permission = { - freely: 'freely', - editable: 'editable', - limited: 'limited', - locked: 'locked', - protected: 'protected', - private: 'private' -} diff --git a/old_src/lib/config/environment.ts b/old_src/lib/config/environment.ts deleted file mode 100644 index c8f99a544..000000000 --- a/old_src/lib/config/environment.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { toArrayConfig, toBooleanConfig, toIntegerConfig } from './utils' - -export const environment = { - sourceURL: process.env.CMD_SOURCE_URL, - domain: process.env.CMD_DOMAIN, - urlPath: process.env.CMD_URL_PATH, - host: process.env.CMD_HOST, - port: toIntegerConfig(process.env.CMD_PORT), - path: process.env.CMD_PATH, - socket: { - group: process.env.CMD_SOCKET_GROUP, - owner: process.env.CMD_SOCKET_OWNER, - mode: process.env.CMD_SOCKET_MODE - }, - loglevel: process.env.CMD_LOGLEVEL, - urlAddPort: toBooleanConfig(process.env.CMD_URL_ADDPORT), - useSSL: toBooleanConfig(process.env.CMD_USESSL), - hsts: { - enable: toBooleanConfig(process.env.CMD_HSTS_ENABLE), - maxAgeSeconds: toIntegerConfig(process.env.CMD_HSTS_MAX_AGE), - includeSubdomains: toBooleanConfig(process.env.CMD_HSTS_INCLUDE_SUBDOMAINS), - preload: toBooleanConfig(process.env.CMD_HSTS_PRELOAD) - }, - csp: { - enable: toBooleanConfig(process.env.CMD_CSP_ENABLE), - reportURI: process.env.CMD_CSP_REPORTURI - }, - protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), - allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN), - useCDN: toBooleanConfig(process.env.CMD_USECDN), - allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS), - allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS), - allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL), - forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS), - defaultPermission: process.env.CMD_DEFAULT_PERMISSION, - dbURL: process.env.CMD_DB_URL, - sessionSecret: process.env.CMD_SESSION_SECRET, - sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE), - tooBusyLag: toIntegerConfig(process.env.CMD_TOOBUSY_LAG), - imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE, - imgur: { - clientID: process.env.CMD_IMGUR_CLIENTID - }, - s3: { - accessKeyId: process.env.CMD_S3_ACCESS_KEY_ID, - secretAccessKey: process.env.CMD_S3_SECRET_ACCESS_KEY, - region: process.env.CMD_S3_REGION, - endpoint: process.env.CMD_S3_ENDPOINT - }, - minio: { - accessKey: process.env.CMD_MINIO_ACCESS_KEY, - secretKey: process.env.CMD_MINIO_SECRET_KEY, - endPoint: process.env.CMD_MINIO_ENDPOINT, - secure: toBooleanConfig(process.env.CMD_MINIO_SECURE), - port: toIntegerConfig(process.env.CMD_MINIO_PORT) - }, - lutim: { - url: process.env.CMD_LUTIM_URL - }, - s3bucket: process.env.CMD_S3_BUCKET, - azure: { - connectionString: process.env.CMD_AZURE_CONNECTION_STRING, - container: process.env.CMD_AZURE_CONTAINER - }, - facebook: { - clientID: process.env.CMD_FACEBOOK_CLIENTID, - clientSecret: process.env.CMD_FACEBOOK_CLIENTSECRET - }, - twitter: { - consumerKey: process.env.CMD_TWITTER_CONSUMERKEY, - consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET - }, - github: { - clientID: process.env.CMD_GITHUB_CLIENTID, - clientSecret: process.env.CMD_GITHUB_CLIENTSECRET - }, - gitlab: { - baseURL: process.env.CMD_GITLAB_BASEURL, - clientID: process.env.CMD_GITLAB_CLIENTID, - clientSecret: process.env.CMD_GITLAB_CLIENTSECRET, - scope: process.env.CMD_GITLAB_SCOPE - }, - oauth2: { - providerName: process.env.CMD_OAUTH2_PROVIDERNAME, - 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, - scope: process.env.CMD_OAUTH2_SCOPE - }, - dropbox: { - clientID: process.env.CMD_DROPBOX_CLIENTID, - clientSecret: process.env.CMD_DROPBOX_CLIENTSECRET, - appKey: process.env.CMD_DROPBOX_APPKEY - }, - google: { - clientID: process.env.CMD_GOOGLE_CLIENTID, - clientSecret: process.env.CMD_GOOGLE_CLIENTSECRET, - hostedDomain: process.env.CMD_GOOGLE_HOSTEDDOMAIN - }, - ldap: { - providerName: process.env.CMD_LDAP_PROVIDERNAME, - url: process.env.CMD_LDAP_URL, - bindDn: process.env.CMD_LDAP_BINDDN, - bindCredentials: process.env.CMD_LDAP_BINDCREDENTIALS, - searchBase: process.env.CMD_LDAP_SEARCHBASE, - searchFilter: process.env.CMD_LDAP_SEARCHFILTER, - searchAttributes: toArrayConfig(process.env.CMD_LDAP_SEARCHATTRIBUTES), - usernameField: process.env.CMD_LDAP_USERNAMEFIELD, - useridField: process.env.CMD_LDAP_USERIDFIELD, - tlsca: process.env.CMD_LDAP_TLS_CA - }, - saml: { - idpSsoUrl: process.env.CMD_SAML_IDPSSOURL, - idpCert: process.env.CMD_SAML_IDPCERT, - issuer: process.env.CMD_SAML_ISSUER, - identifierFormat: process.env.CMD_SAML_IDENTIFIERFORMAT, - disableRequestedAuthnContext: toBooleanConfig(process.env.CMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT), - groupAttribute: process.env.CMD_SAML_GROUPATTRIBUTE, - externalGroups: toArrayConfig(process.env.CMD_SAML_EXTERNALGROUPS, '|', []), - requiredGroups: toArrayConfig(process.env.CMD_SAML_REQUIREDGROUPS, '|', []), - attribute: { - id: process.env.CMD_SAML_ATTRIBUTE_ID, - username: process.env.CMD_SAML_ATTRIBUTE_USERNAME, - email: process.env.CMD_SAML_ATTRIBUTE_EMAIL - } - }, - email: toBooleanConfig(process.env.CMD_EMAIL), - allowEmailRegister: toBooleanConfig(process.env.CMD_ALLOW_EMAIL_REGISTER), - allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR), - openID: toBooleanConfig(process.env.CMD_OPENID), - linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE -} diff --git a/old_src/lib/config/hackmdEnvironment.ts b/old_src/lib/config/hackmdEnvironment.ts deleted file mode 100644 index 19b390b3e..000000000 --- a/old_src/lib/config/hackmdEnvironment.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { toArrayConfig, toBooleanConfig, toIntegerConfig } from './utils' - -export const hackmdEnvironment = { - 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: toIntegerConfig(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 - }, - 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, - scope: process.env.HMD_OAUTH2_SCOPE - }, - 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, - disableRequestedAuthnContext: toBooleanConfig(process.env.HMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT), - 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) -} diff --git a/old_src/lib/config/index.ts b/old_src/lib/config/index.ts deleted file mode 100644 index beaa09d88..000000000 --- a/old_src/lib/config/index.ts +++ /dev/null @@ -1,211 +0,0 @@ -import crypto from 'crypto' -import fs from 'fs' -import path from 'path' -import { merge } from 'lodash' -import { Environment, Permission } from './enum' -import { logger } from '../logger' -import { getGitCommit, getGitHubURL } from './utils' -import { defaultConfig } from './default' -import { defaultSSL } from './defaultSSL' -import { oldDefault } from './oldDefault' -import { oldEnvironment } from './oldEnvironment' -import { hackmdEnvironment } from './hackmdEnvironment' -import { environment } from './environment' -import { dockerSecret } from './dockerSecret' -import deepFreeze = require('deep-freeze') - -const appRootPath = path.resolve(__dirname, '../../../') -const env = process.env.NODE_ENV || Environment.development -const debugConfig = { - debug: (env === Environment.development) -} - -// Get version string from package.json -// TODO: There are other ways to geht the current version -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { version, repository } = require(path.join(appRootPath, 'package.json')) - -const commitID = getGitCommit(appRootPath) -const sourceURL = getGitHubURL(repository.url, commitID || version) -const fullversion = commitID ? `${version}-${commitID}` : version - -const packageConfig = { - version: version, - minimumCompatibleVersion: '0.5.0', - fullversion: fullversion, - sourceURL: sourceURL -} - -const configFilePath = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE || - 'config.json') -const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined -merge(defaultConfig, defaultSSL) -merge(defaultConfig, oldDefault) -merge(defaultConfig, debugConfig) -merge(defaultConfig, packageConfig) -merge(defaultConfig, fileConfig) -merge(defaultConfig, oldEnvironment) -merge(defaultConfig, hackmdEnvironment) -merge(defaultConfig, environment) -merge(defaultConfig, dockerSecret) - -if (['debug', 'verbose', 'info', 'warn', 'error'].includes(defaultConfig.loglevel)) { - logger.level = defaultConfig.loglevel -} else { - logger.error('Selected loglevel %s doesn\'t exist, using default level \'debug\'. Available options: debug, verbose, info, warn, error', defaultConfig.loglevel) -} - -// load LDAP CA -if (defaultConfig.ldap?.tlsca) { - const ca = defaultConfig.ldap.tlsca.split(',') - const caContent: string[] = [] - for (const i of ca) { - if (fs.existsSync(i)) { - caContent.push(fs.readFileSync(i, 'utf8')) - } - } - const tlsOptions = { - ca: caContent - } - defaultConfig.ldap.tlsOptions = defaultConfig.ldap.tlsOptions ? Object.assign(defaultConfig.ldap.tlsOptions, tlsOptions) : tlsOptions -} - -// Permission -defaultConfig.permission = Permission -if (!defaultConfig.allowAnonymous && !defaultConfig.allowAnonymousEdits) { - delete defaultConfig.permission.freely -} -if (!(defaultConfig.defaultPermission in defaultConfig.permission)) { - defaultConfig.defaultPermission = defaultConfig.permission.editable -} - -// cache result, cannot change config in runtime!!! -defaultConfig.isStandardHTTPsPort = (function isStandardHTTPsPort (): boolean { - return defaultConfig.useSSL && defaultConfig.port === 443 -})() -defaultConfig.isStandardHTTPPort = (function isStandardHTTPPort (): boolean { - return !defaultConfig.useSSL && defaultConfig.port === 80 -})() - -// cache serverURL -defaultConfig.serverURL = (function getserverurl (): string { - let url = '' - if (defaultConfig.domain) { - const protocol = defaultConfig.protocolUseSSL ? 'https://' : 'http://' - url = protocol + defaultConfig.domain - if (defaultConfig.urlAddPort) { - if (!defaultConfig.isStandardHTTPPort || !defaultConfig.isStandardHTTPsPort) { - url += ':' + defaultConfig.port - } - } - } - if (defaultConfig.urlPath) { - url += '/' + defaultConfig.urlPath - } - return url -})() - -if (defaultConfig.serverURL === '') { - logger.warn('Neither \'domain\' nor \'CMD_DOMAIN\' is configured. This can cause issues with various components.\nHint: Make sure \'protocolUseSSL\' and \'urlAddPort\' or \'CMD_PROTOCOL_USESSL\' and \'CMD_URL_ADDPORT\' are configured properly.') -} - -defaultConfig.Environment = Environment - -// auth method -defaultConfig.isFacebookEnable = defaultConfig.facebook?.clientID && defaultConfig.facebook.clientSecret -defaultConfig.isGoogleEnable = defaultConfig.google?.clientID && defaultConfig.google.clientSecret -defaultConfig.isDropboxEnable = defaultConfig.dropbox?.clientID && defaultConfig.dropbox.clientSecret -defaultConfig.isTwitterEnable = defaultConfig.twitter?.consumerKey && defaultConfig.twitter.consumerSecret -defaultConfig.isEmailEnable = defaultConfig.email -defaultConfig.isOpenIDEnable = defaultConfig.openID -defaultConfig.isGitHubEnable = defaultConfig.github?.clientID && defaultConfig.github.clientSecret -defaultConfig.isGitLabEnable = defaultConfig.gitlab?.clientID && defaultConfig.gitlab.clientSecret -defaultConfig.isLDAPEnable = defaultConfig.ldap?.url -defaultConfig.isSAMLEnable = defaultConfig.saml?.idpSsoUrl -defaultConfig.isOAuth2Enable = defaultConfig.oauth2?.clientID && defaultConfig.oauth2.clientSecret - -// Check gitlab api version -if (defaultConfig.gitlab && defaultConfig.gitlab.version !== 'v4' && defaultConfig.gitlab.version !== 'v3') { - logger.warn('config.js contains wrong version (' + defaultConfig.gitlab.version + ') for gitlab api; it should be \'v3\' or \'v4\'. Defaulting to v4') - defaultConfig.gitlab.version = 'v4' -} -// If gitlab scope is api, enable snippets Export/import -defaultConfig.isGitlabSnippetsEnable = (!defaultConfig.gitlab?.scope || defaultConfig.gitlab.scope === 'api') && defaultConfig.isGitLabEnable - -// Only update i18n files in development setups -defaultConfig.updateI18nFiles = (env === Environment.development) - -// merge legacy values -const keys = Object.keys(defaultConfig) -const uppercase = /[A-Z]/ -for (let i = keys.length; i--;) { - const lowercaseKey = keys[i].toLowerCase() - // if the config contains uppercase letters - // and a lowercase version of this setting exists - // and the config with uppercase is not set - // we set the new config using the old key. - if (uppercase.test(keys[i]) && - defaultConfig[lowercaseKey] !== undefined && - fileConfig[keys[i]] === undefined) { - logger.warn('config.js contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.js file to replace ' + lowercaseKey + ' with ' + keys[i]) - defaultConfig[keys[i]] = defaultConfig[lowercaseKey] - } -} - -// Notify users about the prefix change and inform them they use legacy prefix for environment variables -if (Object.keys(process.env).toString().includes('HMD_')) { - logger.warn('Using legacy HMD prefix for environment variables. Please change your variables in future. For details see: https://github.com/codimd/server#environment-variables-will-overwrite-other-server-configs') -} - -// Generate session secret if it stays on default values -if (defaultConfig.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.') - defaultConfig.sessionSecret = crypto.randomBytes(Math.ceil(defaultConfig.sessionSecretLen / 2)) // generate crypto graphic random number - .toString('hex') // convert to hexadecimal format - .slice(0, defaultConfig.sessionSecretLen) // return required number of characters -} - -// Validate upload upload providers -if (!['filesystem', 's3', 'minio', 'imgur', 'azure', 'lutim'].includes(defaultConfig.imageUploadType)) { - logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure", "lutim" or "imgur". Defaulting to "filesystem"') - defaultConfig.imageUploadType = 'filesystem' -} - -// figure out mime types for image uploads -switch (defaultConfig.imageUploadType) { - case 'imgur': - defaultConfig.allowedUploadMimeTypes = [ - 'image/jpeg', - 'image/png', - 'image/jpg', - 'image/gif' - ] - break - default: - defaultConfig.allowedUploadMimeTypes = [ - 'image/jpeg', - 'image/png', - 'image/jpg', - 'image/gif', - 'image/svg+xml' - ] -} - -// generate correct path -defaultConfig.sslCAPath.forEach(function (capath, i, array) { - array[i] = path.resolve(appRootPath, capath) -}) - -defaultConfig.sslCertPath = path.resolve(appRootPath, defaultConfig.sslCertPath) -defaultConfig.sslKeyPath = path.resolve(appRootPath, defaultConfig.sslKeyPath) -defaultConfig.dhParamPath = path.resolve(appRootPath, defaultConfig.dhParamPath) -defaultConfig.viewPath = path.resolve(appRootPath, defaultConfig.viewPath) -defaultConfig.tmpPath = path.resolve(appRootPath, defaultConfig.tmpPath) -defaultConfig.publicPath = path.resolve(appRootPath, defaultConfig.publicPath) -defaultConfig.defaultNotePath = path.resolve(appRootPath, defaultConfig.defaultNotePath) -defaultConfig.docsPath = path.resolve(appRootPath, defaultConfig.docsPath) -defaultConfig.uploadsPath = path.resolve(appRootPath, defaultConfig.uploadsPath) -defaultConfig.localesPath = path.resolve(appRootPath, defaultConfig.localesPath) - -// make config readonly -export const config = deepFreeze(defaultConfig) diff --git a/old_src/lib/config/interfaces.ts b/old_src/lib/config/interfaces.ts deleted file mode 100644 index c9593b329..000000000 --- a/old_src/lib/config/interfaces.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Permission } from './enum' -import { IHelmetContentSecurityPolicyDirectives } from 'helmet' - -type CSPDirectives = IHelmetContentSecurityPolicyDirectives - -export interface Config { - permission: Permission; - domain: string; - urlPath: string; - host: string; - port: number; - loglevel: string; - urlAddPort: boolean; - allowOrigin: string[]; - useSSL: boolean; - hsts: { - enable: boolean; - maxAgeSeconds: number; - includeSubdomains: boolean; - preload: boolean; - }; - csp: { - enable: boolean; - directives?: CSPDirectives; - addDefaults: boolean; - addDisqus: boolean; - addGoogleAnalytics: boolean; - upgradeInsecureRequests: string | boolean; - reportURI?: string; - }; - protocolUseSSL: boolean; - useCDN: boolean; - allowAnonymous: boolean; - allowAnonymousEdits: boolean; - allowFreeURL: boolean; - forbiddenNoteIDs: string[]; - defaultPermission: string; - dbURL: string; - db; - sslKeyPath: string; - sslCertPath: string; - sslCAPath: string[]; - dhParamPath: string; - publicPath: string; - viewPath: string; - tmpPath: string; - defaultNotePath: string; - docsPath: string; - uploadsPath: string; - sessionName: string; - sessionSecret: string; - sessionSecretLen: number; - sessionLife: number; - staticCacheTime: number; - heartbeatInterval: number; - heartbeatTimeout: number; - tooBusyLag: number; - documentMaxLength: number; - imageUploadType: 'azure' | 'filesystem' | 'imgur' | 'lutim' | 'minio' | 's3'; - lutim?: { - url: string; - }; - imgur?: { - clientID: string; - }; - s3?: { - accessKeyId: string; - secretAccessKey: string; - region: string; - }; - minio?: { - accessKey?: string; - secretKey?: string; - endPoint?: string; - secure?: boolean; - port?: number; - }; - s3bucket?: string; - azure?: { - connectionString: string; - container: string; - }; - oauth2?: { - providerName: string; - authorizationURL: string; - tokenURL: string; - clientID: string; - clientSecret: string; - }; - facebook?: { - clientID: string; - clientSecret: string; - }; - twitter?: { - consumerKey: string; - consumerSecret: string; - }; - github?: { - clientID: string; - clientSecret: string; - }; - gitlab?: { - baseURL?: string; - clientID?: string; - clientSecret?: string; - scope?: string; - version?: string; - }; - dropbox?: { - clientID: string; - clientSecret: string; - appKey: string; - }; - google?: { - clientID: string; - clientSecret: string; - hostedDomain: string; - }; - ldap?: { - providerName: string; - url: string; - bindDn: string; - bindCredentials: string; - searchBase: string; - searchFilter: string; - searchAttributes: string; - usernameField: string; - useridField: string; - tlsca: string; - starttls?: boolean; - tlsOptions: { - ca: string[]; - }; - }; - saml?: { - idpSsoUrl?: string; - idpCert?: string; - issuer?: string; - identifierFormat?: string; - disableRequestedAuthnContext?: boolean; - groupAttribute?: string; - externalGroups?: string[]; - requiredGroups?: string[]; - attribute?: { - id?: string; - username?: string; - email?: string; - }; - }; - email: boolean; - allowEmailRegister: boolean; - allowGravatar: boolean; - openID: boolean; - linkifyHeaderStyle: string; - - // TODO: Remove escape hatch for dynamically added properties - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [propName: string]: any; -} diff --git a/old_src/lib/config/oldDefault.ts b/old_src/lib/config/oldDefault.ts deleted file mode 100644 index 7c350bb62..000000000 --- a/old_src/lib/config/oldDefault.ts +++ /dev/null @@ -1,39 +0,0 @@ -export const oldDefault = { - urlpath: undefined, - urladdport: undefined, - alloworigin: undefined, - usessl: undefined, - protocolusessl: undefined, - usecdn: undefined, - allowanonymous: undefined, - allowanonymousedits: undefined, - allowfreeurl: undefined, - defaultpermission: undefined, - dburl: undefined, - // ssl path - sslkeypath: undefined, - sslcertpath: undefined, - sslcapath: undefined, - dhparampath: undefined, - // other path - tmppath: undefined, - defaultnotepath: undefined, - docspath: undefined, - indexpath: undefined, - hackmdpath: undefined, - errorpath: undefined, - prettypath: undefined, - slidepath: undefined, - // session - sessionname: undefined, - sessionsecret: undefined, - sessionlife: undefined, - staticcachetime: undefined, - // socket.io - heartbeatinterval: undefined, - heartbeattimeout: undefined, - // document - documentmaxlength: undefined, - imageuploadtype: undefined, - allowemailregister: undefined -} diff --git a/old_src/lib/config/oldEnvironment.ts b/old_src/lib/config/oldEnvironment.ts deleted file mode 100644 index f68bb3183..000000000 --- a/old_src/lib/config/oldEnvironment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { toBooleanConfig } from './utils' - -export const oldEnvironment = { - debug: toBooleanConfig(process.env.DEBUG), - dburl: process.env.DATABASE_URL, - urlpath: process.env.URL_PATH, - port: process.env.PORT -} diff --git a/old_src/lib/config/utils.ts b/old_src/lib/config/utils.ts deleted file mode 100644 index 56cb5137e..000000000 --- a/old_src/lib/config/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import fs from 'fs' -import path from 'path' - -export function toBooleanConfig (configValue: string | boolean | undefined): boolean | undefined { - if (typeof configValue === 'string') { - return (configValue === 'true') - } - return configValue -} - -export function toArrayConfig (configValue: string | undefined, separator = ',', fallback = []): string[] { - if (configValue) { - return (configValue.split(separator).map(arrayItem => arrayItem.trim())) - } - return fallback -} - -export function toIntegerConfig (configValue): number { - if (configValue && typeof configValue === 'string') { - return parseInt(configValue) - } - return configValue -} - -export function getGitCommit (repodir): string { - if (!fs.existsSync(repodir + '/.git/HEAD')) { - return '' - } - let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8') - if (reference.startsWith('ref: ')) { - reference = reference.substr(5).replace('\n', '') - reference = fs.readFileSync(path.resolve(repodir + '/.git', reference), 'utf8') - } - reference = reference.replace('\n', '') - return reference -} - -export function getGitHubURL (repo, reference): string { - // if it's not a github reference, we handle handle that anyway - if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) { - return repo - } - if (repo.startsWith('git@github.com') || repo.startsWith('ssh://git@github.com')) { - repo = repo.replace(/^(ssh:\/\/)?git@github.com:/, 'https://github.com/') - } - - if (repo.endsWith('.git')) { - repo = repo.replace(/\.git$/, '/') - } else if (!repo.endsWith('/')) { - repo = repo + '/' - } - return repo + 'tree/' + reference -} diff --git a/old_src/lib/csp.ts b/old_src/lib/csp.ts deleted file mode 100644 index 0fac8faa4..000000000 --- a/old_src/lib/csp.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { config } from './config' -import { IHelmetContentSecurityPolicyDirectives } from 'helmet' -import uuid from 'uuid' -import { NextFunction, Request, Response } from 'express' - -type CSPDirectives = IHelmetContentSecurityPolicyDirectives - -const defaultDirectives = { - defaultSrc: ['\'self\''], - scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''], - // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/codimd/issues/594 - imgSrc: ['*'], - styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views - fontSrc: ['\'self\'', 'data:', 'https://public.slidesharecdn.com'], - objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ - mediaSrc: ['*'], - childSrc: ['*'], - connectSrc: ['*'] -} - -const cdnDirectives = { - scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], - styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'], - fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] -} - -const disqusDirectives = { - scriptSrc: ['https://disqus.com', 'https://*.disqus.com', 'https://*.disquscdn.com'], - styleSrc: ['https://*.disquscdn.com'], - fontSrc: ['https://*.disquscdn.com'] -} - -const googleAnalyticsDirectives = { - scriptSrc: ['https://www.google-analytics.com'] -} - -function mergeDirectives (existingDirectives: CSPDirectives, newDirectives: CSPDirectives): void { - for (const propertyName in newDirectives) { - const newDirective = newDirectives[propertyName] - if (newDirective) { - const existingDirective = existingDirectives[propertyName] || [] - existingDirectives[propertyName] = existingDirective.concat(newDirective) - } - } -} - -function mergeDirectivesIf (condition: boolean, existingDirectives: CSPDirectives, newDirectives: CSPDirectives): void { - if (condition) { - mergeDirectives(existingDirectives, newDirectives) - } -} - -function areAllInlineScriptsAllowed (directives: CSPDirectives): boolean { - if (directives.scriptSrc) { - return directives.scriptSrc.includes('\'unsafe-inline\'') - } - return false -} - -function getCspNonce (req: Request, res: Response): string { - return "'nonce-" + res.locals.nonce + "'" -} - -function addInlineScriptExceptions (directives: CSPDirectives): void { - if (!directives.scriptSrc) { - directives.scriptSrc = [] - } - directives.scriptSrc.push(getCspNonce) - // TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html - // Any more clean solution appreciated. - directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'') -} - -function addUpgradeUnsafeRequestsOptionTo (directives: CSPDirectives): void { - if (config.csp.upgradeInsecureRequests === 'auto' && config.useSSL) { - directives.upgradeInsecureRequests = true - } else if (config.csp.upgradeInsecureRequests === true) { - directives.upgradeInsecureRequests = true - } -} - -function addReportURI (directives): void { - if (config.csp.reportURI) { - directives.reportUri = config.csp.reportURI - } -} - -export function addNonceToLocals (req: Request, res: Response, next: NextFunction): void { - res.locals.nonce = uuid.v4() - next() -} - -export function computeDirectives (): CSPDirectives { - const directives: CSPDirectives = {} - mergeDirectives(directives, config.csp.directives) - mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives) - mergeDirectivesIf(config.useCDN, directives, cdnDirectives) - mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives) - mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives) - if (!areAllInlineScriptsAllowed(directives)) { - addInlineScriptExceptions(directives) - } - addUpgradeUnsafeRequestsOptionTo(directives) - addReportURI(directives) - return directives -} diff --git a/old_src/lib/errors.ts b/old_src/lib/errors.ts deleted file mode 100644 index 4adc871b9..000000000 --- a/old_src/lib/errors.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { config } from './config' - -function responseError (res, code: number, detail: string, msg: string): void { - res.status(code).render('error.ejs', { - title: code + ' ' + detail + ' ' + msg, - code: code, - detail: detail, - msg: msg, - opengraph: [] - }) -} - -function errorForbidden (res): void { - const { req } = res - if (req.user) { - responseError(res, 403, 'Forbidden', 'oh no.') - } else { - if (!req.session) req.session = {} - req.session.returnTo = req.originalUrl || config.serverUrl + '/' - req.flash('error', 'You are not allowed to access this page. Maybe try logging in?') - res.redirect(config.serverURL + '/') - } -} - -function errorNotFound (res): void { - responseError(res, 404, 'Not Found', 'oops.') -} - -function errorBadRequest (res): void { - responseError(res, 400, 'Bad Request', 'something not right.') -} - -function errorTooLong (res): void { - responseError(res, 413, 'Payload Too Large', 'Shorten your note!') -} - -function errorInternalError (res): void { - responseError(res, 500, 'Internal Error', 'wtf.') -} - -function errorServiceUnavailable (res): void { - responseError(res, 503, 'Service Unvavilable', 'I\'m busy right now, try again later.') -} - -const errors = { - errorForbidden: errorForbidden, - errorNotFound: errorNotFound, - errorBadRequest: errorBadRequest, - errorTooLong: errorTooLong, - errorInternalError: errorInternalError, - errorServiceUnavailable: errorServiceUnavailable -} - -export { errors } diff --git a/old_src/lib/history.ts b/old_src/lib/history.ts deleted file mode 100644 index 6bc8ad4fb..000000000 --- a/old_src/lib/history.ts +++ /dev/null @@ -1,208 +0,0 @@ -// history -// external modules -import LZString from 'lz-string' - -// core -import { logger } from './logger' -import { Note, User } from './models' -import { errors } from './errors' -import { LogEntry } from 'winston' - -// public - -class HistoryObject { - id: string - text: string - time: number - tags: string[] - pinned?: boolean -} - -function parseHistoryMapToArray (historyMap: Map): HistoryObject[] { - const historyArray: HistoryObject[] = [] - for (const [, value] of historyMap) { - historyArray.push(value) - } - return historyArray -} - -function parseHistoryArrayToMap (historyArray: HistoryObject[]): Map { - const historyMap = new Map() - for (let i = 0; i < historyArray.length; i++) { - const item = historyArray[i] - historyMap.set(item.id, item) - } - return historyMap -} - -function getHistory (userId, callback: (err: unknown, history: Map | null) => void): void { - User.findOne({ - where: { - id: userId - } - }).then(function (user) { - if (!user) { - return callback(null, null) - } - if (user.history) { - const history: HistoryObject[] = JSON.parse(user.history) - // migrate LZString encoded note id to base64url encoded note id - for (let i = 0, l = history.length; i < l; i++) { - // Calculate minimal string length for an UUID that is encoded - // base64 encoded and optimize comparsion by using -1 - // this should make a lot of LZ-String parsing errors obsolete - // as we can assume that a nodeId that is 48 chars or longer is a - // noteID. - const base64UuidLength = ((4 * 36) / 3) - 1 - if (!(history[i].id.length > base64UuidLength)) { - continue - } - try { - const id = LZString.decompressFromBase64(history[i].id) - if (id && Note.checkNoteIdValid(id)) { - history[i].id = Note.encodeNoteId(id) - } - } catch (err) { - // most error here comes from LZString, ignore - if (err.message === 'Cannot read property \'charAt\' of undefined') { - logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.') - } else { - logger.error(err) - } - } - } - logger.debug(`read history success: ${user.id}`) - return callback(null, parseHistoryArrayToMap(history)) - } - logger.debug(`read empty history: ${user.id}`) - return callback(null, new Map()) - }).catch(function (err) { - logger.error('read history failed: ' + err) - return callback(err, null) - }) -} - -function setHistory (userId: string, history: HistoryObject[], callback: (err: LogEntry | null, count: [number, User[]] | null) => void): void { - User.update({ - history: JSON.stringify(history) - }, { - where: { - id: userId - } - }).then(function (count) { - return callback(null, count) - }).catch(function (err) { - logger.error('set history failed: ' + err) - return callback(err, null) - }) -} - -function updateHistory (userId: string, noteId: string, document, time): void { - if (userId && noteId && typeof document !== 'undefined') { - getHistory(userId, function (err, history) { - if (err || !history) return - const noteHistory = history.get(noteId) || new HistoryObject() - const noteInfo = Note.parseNoteInfo(document) - noteHistory.id = noteId - noteHistory.text = noteInfo.title - noteHistory.time = time || Date.now() - noteHistory.tags = noteInfo.tags - history.set(noteId, noteHistory) - setHistory(userId, parseHistoryMapToArray(history), function (err, _) { - if (err) { - logger.log(err) - } - }) - }) - } -} - -function historyGet (req, res): void { - if (req.isAuthenticated()) { - getHistory(req.user.id, function (err, history) { - if (err) return errors.errorInternalError(res) - if (!history) return errors.errorNotFound(res) - res.send({ - history: parseHistoryMapToArray(history) - }) - }) - } else { - return errors.errorForbidden(res) - } -} - -function historyPost (req, res): void { - if (req.isAuthenticated()) { - const noteId = req.params.noteId - if (!noteId) { - if (typeof req.body.history === 'undefined') return errors.errorBadRequest(res) - logger.debug(`SERVER received history from [${req.user.id}]: ${req.body.history}`) - let history - try { - history = JSON.parse(req.body.history) - } catch (err) { - return errors.errorBadRequest(res) - } - if (Array.isArray(history)) { - setHistory(req.user.id, history, function (err, _) { - if (err) return errors.errorInternalError(res) - res.end() - }) - } else { - return errors.errorBadRequest(res) - } - } else { - if (typeof req.body.pinned === 'undefined') return errors.errorBadRequest(res) - getHistory(req.user.id, function (err, history) { - if (err) return errors.errorInternalError(res) - if (!history) return errors.errorNotFound(res) - const noteHistory = history.get(noteId) - if (!noteHistory) return errors.errorNotFound(res) - if (req.body.pinned === 'true' || req.body.pinned === 'false') { - noteHistory.pinned = (req.body.pinned === 'true') - setHistory(req.user.id, parseHistoryMapToArray(history), function (err, _) { - if (err) return errors.errorInternalError(res) - res.end() - }) - } else { - return errors.errorBadRequest(res) - } - }) - } - } else { - return errors.errorForbidden(res) - } -} - -function historyDelete (req, res): void { - if (req.isAuthenticated()) { - const noteId = req.params.noteId - if (!noteId) { - setHistory(req.user.id, [], function (err, _) { - if (err) return errors.errorInternalError(res) - res.end() - }) - } else { - getHistory(req.user.id, function (err, history) { - if (err) return errors.errorInternalError(res) - if (!history) return errors.errorNotFound(res) - history.delete(noteId) - setHistory(req.user.id, parseHistoryMapToArray(history), function (err, _) { - if (err) return errors.errorInternalError(res) - res.end() - }) - }) - } - } else { - return errors.errorForbidden(res) - } -} - -const History = { - historyGet: historyGet, - historyPost: historyPost, - historyDelete: historyDelete, - updateHistory: updateHistory -} - -export { History, HistoryObject } diff --git a/old_src/lib/letter-avatars.ts b/old_src/lib/letter-avatars.ts deleted file mode 100644 index e476ad6bb..000000000 --- a/old_src/lib/letter-avatars.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createHash } from 'crypto' -import randomColor from 'randomcolor' -import { config } from './config' - -// core -export function generateAvatar (name: string): string { - const color = randomColor({ - seed: name, - luminosity: 'dark' - }) - const letter = name.substring(0, 1).toUpperCase() - - let svg = '' - svg += '' - svg += '' - svg += '' - svg += '' - svg += '' + letter + '' - svg += '' - svg += '' - svg += '' - - return svg -} - -export function generateAvatarURL (name: string, email = '', big = true): string { - let photo - name = encodeURIComponent(name) - - const hash = createHash('md5') - hash.update(email.toLowerCase()) - const hexDigest = hash.digest('hex') - - if (email !== '' && config.allowGravatar) { - photo = 'https://cdn.libravatar.org/avatar/' + hexDigest - if (big) { - photo += '?s=400' - } else { - photo += '?s=96' - } - } else { - photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || hexDigest) + '/avatar.svg' - } - return photo -} diff --git a/old_src/lib/library-ext.d.ts b/old_src/lib/library-ext.d.ts deleted file mode 100644 index e06789e84..000000000 --- a/old_src/lib/library-ext.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { User } from './models' - -declare module 'express' { - export interface Request { - user?: User; - flash (type: string, msg?: string): [] | object | number; - } -} diff --git a/old_src/lib/logger.ts b/old_src/lib/logger.ts deleted file mode 100644 index 8711bbd1b..000000000 --- a/old_src/lib/logger.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createLogger, format, transports } from 'winston' - -const logger = createLogger({ - level: 'debug', - format: format.combine( - format.uncolorize(), - format.timestamp(), - format.align(), - format.splat(), - format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`) - ), - transports: [ - new transports.Console({ - handleExceptions: true - }) - ], - exitOnError: false -}) - -export { logger } diff --git a/old_src/lib/migrations/20150504155329-create-users.js b/old_src/lib/migrations/20150504155329-create-users.js deleted file mode 100644 index 56dec7097..000000000 --- a/old_src/lib/migrations/20150504155329-create-users.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.createTable('Users', { - id: { - type: Sequelize.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - profileid: { - type: Sequelize.STRING, - unique: true - }, - profile: Sequelize.TEXT, - history: Sequelize.TEXT, - createdAt: Sequelize.DATE, - updatedAt: Sequelize.DATE - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.dropTable('Users') - } -} diff --git a/old_src/lib/migrations/20150508114741-create-notes.js b/old_src/lib/migrations/20150508114741-create-notes.js deleted file mode 100644 index 975919b6b..000000000 --- a/old_src/lib/migrations/20150508114741-create-notes.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.createTable('Notes', { - id: { - type: Sequelize.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - ownerId: Sequelize.UUID, - content: Sequelize.TEXT, - title: Sequelize.STRING, - createdAt: Sequelize.DATE, - updatedAt: Sequelize.DATE - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.dropTable('Notes') - } -} diff --git a/old_src/lib/migrations/20150515125813-create-temp.js b/old_src/lib/migrations/20150515125813-create-temp.js deleted file mode 100644 index d1e0265c9..000000000 --- a/old_src/lib/migrations/20150515125813-create-temp.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.createTable('Temp', { - id: { - type: Sequelize.STRING, - primaryKey: true - }, - date: Sequelize.TEXT, - createdAt: Sequelize.DATE, - updatedAt: Sequelize.DATE - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.dropTable('Temp') - } -} diff --git a/old_src/lib/migrations/20150702001020-update-to-0_3_1.js b/old_src/lib/migrations/20150702001020-update-to-0_3_1.js deleted file mode 100644 index 5152c8a8d..000000000 --- a/old_src/lib/migrations/20150702001020-update-to-0_3_1.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'shortid', { - type: Sequelize.STRING, - defaultValue: '0000000000', - allowNull: false - }).then(function () { - return queryInterface.addIndex('Notes', ['shortid'], { - indicesType: 'UNIQUE' - }) - }).then(function () { - return queryInterface.addColumn('Notes', 'permission', { - type: Sequelize.STRING, - defaultValue: 'private', - allowNull: false - }) - }).then(function () { - return queryInterface.addColumn('Notes', 'viewcount', { - type: Sequelize.INTEGER, - defaultValue: 0 - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Notes', 'viewcount') - .then(function () { - return queryInterface.removeColumn('Notes', 'permission') - }) - .then(function () { - return queryInterface.removeIndex('Notes', ['shortid']) - }) - .then(function () { - return queryInterface.removeColumn('Notes', 'shortid') - }) - } -} diff --git a/old_src/lib/migrations/20150915153700-change-notes-title-to-text.js b/old_src/lib/migrations/20150915153700-change-notes-title-to-text.js deleted file mode 100644 index d7398b44a..000000000 --- a/old_src/lib/migrations/20150915153700-change-notes-title-to-text.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' -function isSQLite (sequelize) { - return sequelize.options.dialect === 'sqlite' -} - -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Notes', 'title', { - type: Sequelize.TEXT - }).then(function () { - if (isSQLite(queryInterface.sequelize)) { - // manual added index will be removed in sqlite - return queryInterface.addIndex('Notes', ['shortid']) - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Notes', 'title', { - type: Sequelize.STRING - }).then(function () { - if (isSQLite(queryInterface.sequelize)) { - // manual added index will be removed in sqlite - return queryInterface.addIndex('Notes', ['shortid']) - } - }) - } -} diff --git a/old_src/lib/migrations/20160112220142-note-add-lastchange.js b/old_src/lib/migrations/20160112220142-note-add-lastchange.js deleted file mode 100644 index cf67c2f04..000000000 --- a/old_src/lib/migrations/20160112220142-note-add-lastchange.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'lastchangeuserId', { - type: Sequelize.UUID - }).then(function () { - return queryInterface.addColumn('Notes', 'lastchangeAt', { - type: Sequelize.DATE - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Notes', 'lastchangeAt') - .then(function () { - return queryInterface.removeColumn('Notes', 'lastchangeuserId') - }) - } -} diff --git a/old_src/lib/migrations/20160420180355-note-add-alias.js b/old_src/lib/migrations/20160420180355-note-add-alias.js deleted file mode 100644 index 0ed09a3ad..000000000 --- a/old_src/lib/migrations/20160420180355-note-add-alias.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'alias', { - type: Sequelize.STRING - }).then(function () { - return queryInterface.addIndex('Notes', ['alias'], { - indicesType: 'UNIQUE' - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Notes', 'alias').then(function () { - return queryInterface.removeIndex('Notes', ['alias']) - }) - } -} diff --git a/old_src/lib/migrations/20160515114000-user-add-tokens.js b/old_src/lib/migrations/20160515114000-user-add-tokens.js deleted file mode 100644 index d61629631..000000000 --- a/old_src/lib/migrations/20160515114000-user-add-tokens.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () { - return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Users', 'accessToken').then(function () { - return queryInterface.removeColumn('Users', 'refreshToken') - }) - } -} diff --git a/old_src/lib/migrations/20160607060246-support-revision.js b/old_src/lib/migrations/20160607060246-support-revision.js deleted file mode 100644 index 5ec3e52e4..000000000 --- a/old_src/lib/migrations/20160607060246-support-revision.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE).then(function () { - return queryInterface.createTable('Revisions', { - id: { - type: Sequelize.UUID, - primaryKey: true - }, - noteId: Sequelize.UUID, - patch: Sequelize.TEXT, - lastContent: Sequelize.TEXT, - content: Sequelize.TEXT, - length: Sequelize.INTEGER, - createdAt: Sequelize.DATE, - updatedAt: Sequelize.DATE - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.dropTable('Revisions').then(function () { - return queryInterface.removeColumn('Notes', 'savedAt') - }) - } -} diff --git a/old_src/lib/migrations/20160703062241-support-authorship.js b/old_src/lib/migrations/20160703062241-support-authorship.js deleted file mode 100644 index bc16d2fdd..000000000 --- a/old_src/lib/migrations/20160703062241-support-authorship.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT).then(function () { - return queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT) - }).then(function () { - return queryInterface.createTable('Authors', { - id: { - type: Sequelize.INTEGER, - primaryKey: true, - autoIncrement: true - }, - color: Sequelize.STRING, - noteId: Sequelize.UUID, - userId: Sequelize.UUID, - createdAt: Sequelize.DATE, - updatedAt: Sequelize.DATE - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.dropTable('Authors').then(function () { - return queryInterface.removeColumn('Revisions', 'authorship') - }).then(function () { - return queryInterface.removeColumn('Notes', 'authorship') - }) - } -} diff --git a/old_src/lib/migrations/20161009040430-support-delete-note.js b/old_src/lib/migrations/20161009040430-support-delete-note.js deleted file mode 100644 index c15e407de..000000000 --- a/old_src/lib/migrations/20161009040430-support-delete-note.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Notes', 'deletedAt') - } -} diff --git a/old_src/lib/migrations/20161201050312-support-email-signin.js b/old_src/lib/migrations/20161201050312-support-email-signin.js deleted file mode 100644 index 0b52d1e76..000000000 --- a/old_src/lib/migrations/20161201050312-support-email-signin.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () { - return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) { - if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }).catch(function (error) { - if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') { - // eslint-disable-next-line no-console - console.log('Migration has already run… ignoring.') - } else { - throw error - } - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Users', 'email').then(function () { - return queryInterface.removeColumn('Users', 'password') - }) - } -} diff --git a/old_src/lib/migrations/20171009121200-longtext-for-mysql.js b/old_src/lib/migrations/20171009121200-longtext-for-mysql.js deleted file mode 100644 index 96bf7e876..000000000 --- a/old_src/lib/migrations/20171009121200-longtext-for-mysql.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') }) - await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') }) - await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') }) - await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') }) - }, - - down: async function (queryInterface, Sequelize) { - await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT }) - await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT }) - await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT }) - await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT }) - } -} diff --git a/old_src/lib/migrations/20180209120907-longtext-of-authorship.js b/old_src/lib/migrations/20180209120907-longtext-of-authorship.js deleted file mode 100644 index 04810009e..000000000 --- a/old_src/lib/migrations/20180209120907-longtext-of-authorship.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -module.exports = { - up: async function (queryInterface, Sequelize) { - await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') }) - await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') }) - }, - - down: async function (queryInterface, Sequelize) { - await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT }) - await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT }) - } -} diff --git a/old_src/lib/migrations/20180306150303-fix-enum.js b/old_src/lib/migrations/20180306150303-fix-enum.js deleted file mode 100644 index 59a36f21f..000000000 --- a/old_src/lib/migrations/20180306150303-fix-enum.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') }) - } -} diff --git a/old_src/lib/migrations/20180326103000-use-text-in-tokens.js b/old_src/lib/migrations/20180326103000-use-text-in-tokens.js deleted file mode 100644 index 5793cf0e0..000000000 --- a/old_src/lib/migrations/20180326103000-use-text-in-tokens.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Users', 'accessToken', { - type: Sequelize.TEXT - }).then(function () { - return queryInterface.changeColumn('Users', 'refreshToken', { - type: Sequelize.TEXT - }) - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.changeColumn('Users', 'accessToken', { - type: Sequelize.STRING - }).then(function () { - return queryInterface.changeColumn('Users', 'refreshToken', { - type: Sequelize.STRING - }) - }) - } -} diff --git a/old_src/lib/migrations/20180525153000-user-add-delete-token.js b/old_src/lib/migrations/20180525153000-user-add-delete-token.js deleted file mode 100644 index d4287de14..000000000 --- a/old_src/lib/migrations/20180525153000-user-add-delete-token.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' -module.exports = { - up: async function (queryInterface, Sequelize) { - return queryInterface.addColumn('Users', 'deleteToken', { - type: Sequelize.UUID, - defaultValue: Sequelize.UUIDV4 - }) - }, - - down: async function (queryInterface, Sequelize) { - return queryInterface.removeColumn('Users', 'deleteToken') - } -} diff --git a/old_src/lib/models/author.ts b/old_src/lib/models/author.ts deleted file mode 100644 index d4d092f83..000000000 --- a/old_src/lib/models/author.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - AutoIncrement, - BelongsTo, - Column, - createIndexDecorator, - DataType, - ForeignKey, - Model, - PrimaryKey, - Table -} from 'sequelize-typescript' -import { Note, User } from './index' - -const NoteUserIndex = createIndexDecorator({ unique: true }) - -@Table -export class Author extends Model { - @PrimaryKey - @AutoIncrement - @Column(DataType.INTEGER) - id: number - - @Column(DataType.STRING) - color: string - - @ForeignKey(() => Note) - @NoteUserIndex - @Column(DataType.UUID) - noteId: string - - @BelongsTo(() => Note, { foreignKey: 'noteId', onDelete: 'CASCADE', constraints: false, hooks: true }) - note: Note - - @ForeignKey(() => User) - @NoteUserIndex - @Column(DataType.UUID) - userId: string - - @BelongsTo(() => User, { foreignKey: 'userId', onDelete: 'CASCADE', constraints: false, hooks: true }) - user: User -} diff --git a/old_src/lib/models/index.ts b/old_src/lib/models/index.ts deleted file mode 100644 index d96e570fe..000000000 --- a/old_src/lib/models/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Sequelize } from 'sequelize-typescript' -import { cloneDeep } from 'lodash' -import * as path from 'path' -import { Author } from './author' -import { Note } from './note' -import { Revision } from './revision' -import { Temp } from './temp' -import { User } from './user' -import { logger } from '../logger' -import { config } from '../config' -import Umzug from 'umzug' -import SequelizeTypes from 'sequelize' - -const dbconfig = cloneDeep(config.db) -dbconfig.logging = config.debug ? (data): void => { - logger.info(data) -} : false - -export let sequelize: Sequelize - -// Heroku specific -if (config.dbURL) { - sequelize = new Sequelize(config.dbURL, dbconfig) -} else { - sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig) -} - -const umzug = new Umzug({ - migrations: { - path: path.resolve(__dirname, '..', 'migrations'), - params: [ - sequelize.getQueryInterface(), - SequelizeTypes - ] - }, - // Required wrapper function required to prevent winstion issue - // https://github.com/winstonjs/winston/issues/1577 - logging: (message): void => { - logger.info(message) - }, - storage: 'sequelize', - storageOptions: { - sequelize: sequelize - } -}) - -export async function runMigrations (): Promise { - // checks migrations and run them if they are not already applied - // exit in case of unsuccessful migrations - await umzug.up().catch(error => { - logger.error(error) - logger.error('Database migration failed. Exiting…') - process.exit(1) - }) - logger.info('All migrations performed successfully') -} - -sequelize.addModels([Author, Note, Revision, Temp, User]) - -export { Author, Note, Revision, Temp, User } diff --git a/old_src/lib/models/note.ts b/old_src/lib/models/note.ts deleted file mode 100644 index ecd85a06b..000000000 --- a/old_src/lib/models/note.ts +++ /dev/null @@ -1,672 +0,0 @@ -import async from 'async' -import base64url from 'base64url' -import cheerio from 'cheerio' -// eslint-disable-next-line @typescript-eslint/camelcase -import { diff_match_patch, patch_obj } from 'diff-match-patch' -import fs from 'fs' -import LZString from 'lz-string' -import markdownIt from 'markdown-it' -import metaMarked from 'meta-marked' -import moment from 'moment' -import path from 'path' -import Sequelize from 'sequelize' -import { - AfterCreate, - AllowNull, - BeforeCreate, - BelongsTo, - Column, - DataType, - Default, - ForeignKey, - HasMany, - Model, - PrimaryKey, - Table, - Unique -} from 'sequelize-typescript' - -import { generate as shortIdGenerate, isValid as shortIdIsValid } from 'shortid' -import S from 'string' -import { config } from '../config' -import { logger } from '../logger' -import ot from '../ot/index' -import { processData, stripNullByte } from '../utils/functions' -import { Author, Revision, User } from './index' - -const md = markdownIt() -// eslint-disable-next-line new-cap -const dmp = new diff_match_patch() - -// permission types -enum PermissionEnum { - freely = 'freely', - editable = 'editable', - limited = 'limited', - locked = 'locked', - protected = 'protected', - private = 'private' -} - -export class OpengraphMetadata { - title: string | number - description: string | number - type: string -} - -export class NoteMetadata { - title: string - description: string - robots: string - GA: string - disqus: string - slideOptions - opengraph: OpengraphMetadata -} - -export type NoteAuthorship = [string, number, number, number, number] - -@Table({ paranoid: false }) -export class Note extends Model { - @PrimaryKey - @Default(Sequelize.UUIDV4) - @Column(DataType.UUID) - id: string - - @AllowNull(false) - @Default(shortIdGenerate) - @Unique - @Column(DataType.STRING) - shortid: string - - @Unique - @Column(DataType.STRING) - alias: string - - @Column(DataType.ENUM({ values: Object.keys(PermissionEnum).map(k => PermissionEnum[k]) })) - permission: PermissionEnum - - @AllowNull(false) - @Default(0) - @Column(DataType.INTEGER) - viewcount: number - - // ToDo: use @UpdatedAt instead? (https://www.npmjs.com/package/sequelize-typescript#createdat--updatedat--deletedat) - @Column(DataType.DATE) - lastchangeAt: Date - - // ToDo: use @UpdatedAt instead? (https://www.npmjs.com/package/sequelize-typescript#createdat--updatedat--deletedat) - @Column(DataType.DATE) - savedAt: Date - - @ForeignKey(() => User) - @Column - ownerId: string - - @BelongsTo(() => User, { foreignKey: 'ownerId', constraints: false, onDelete: 'CASCADE', hooks: true }) - owner: User - - @ForeignKey(() => User) - @Column - lastchangeuserId: string - - @BelongsTo(() => User, { foreignKey: 'lastchangeuserId', constraints: false }) - lastchangeuser: User - - @HasMany(() => Revision, { foreignKey: 'noteId', constraints: false }) - revisions: Revision[] - - @HasMany(() => Author, { foreignKey: 'noteId', constraints: false }) - authors: Author[] - - @Column(DataType.TEXT) - get title (): string { - return this.getDataValue('title') ?? '' - } - - set title (value: string) { - this.setDataValue('title', stripNullByte(value)) - } - - @Column(DataType.TEXT({ length: 'long' })) - get content (): string { - return this.getDataValue('content') ?? '' - } - - set content (value: string) { - this.setDataValue('content', stripNullByte(value)) - } - - @Column(DataType.TEXT({ length: 'long' })) - get authorship (): NoteAuthorship[] { - return processData(this.getDataValue('authorship'), [], JSON.parse) - } - - set authorship (value: NoteAuthorship[]) { - // Evil hack for TypeScript to accept saving a string in a NoteAuthorship DB-field - this.setDataValue('authorship', JSON.stringify(value) as unknown as NoteAuthorship[]) - } - - @BeforeCreate - static async defaultContentAndPermissions (note: Note): Promise { - return await new Promise(function (resolve) { - // if no content specified then use default note - if (!note.content) { - let filePath: string - if (!note.alias) { - filePath = config.defaultNotePath - } else { - filePath = path.join(config.docsPath, note.alias + '.md') - } - if (Note.checkFileExist(filePath)) { - const fsCreatedTime = moment(fs.statSync(filePath).ctime) - const body = fs.readFileSync(filePath, 'utf8') - note.title = Note.parseNoteTitle(body) - note.content = body - if (filePath !== config.defaultNotePath) { - note.createdAt = fsCreatedTime - } - } - } - // if no permission specified and have owner then give default permission in config, else default permission is freely - if (!note.permission) { - if (note.ownerId) { - // TODO: Might explode if the user-defined permission does not exist - note.permission = PermissionEnum[config.defaultPermission] - } else { - note.permission = PermissionEnum.freely - } - } - return resolve(note) - }) - } - - @AfterCreate - static saveRevision (note): Promise { - return new Promise(function (resolve, reject) { - Revision.saveNoteRevision(note, function (err, _) { - if (err) { - return reject(err) - } - return resolve(note) - }) - }) - } - - static checkFileExist (filePath): boolean { - try { - return fs.statSync(filePath).isFile() - } catch (err) { - return false - } - } - - static encodeNoteId (id): string { - // remove dashes in UUID and encode in url-safe base64 - const str = id.replace(/-/g, '') - const hexStr = Buffer.from(str, 'hex') - return base64url.encode(hexStr) - } - - static decodeNoteId (encodedId): string { - // decode from url-safe base64 - const id: string = base64url.toBuffer(encodedId).toString('hex') - // add dashes between the UUID string parts - const idParts: string[] = [] - idParts.push(id.substr(0, 8)) - idParts.push(id.substr(8, 4)) - idParts.push(id.substr(12, 4)) - idParts.push(id.substr(16, 4)) - idParts.push(id.substr(20, 12)) - return idParts.join('-') - } - - static checkNoteIdValid (id): boolean { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i - const result = id.match(uuidRegex) - return !!(result && result.length === 1) - } - - static parseNoteId (noteId, callback): void { - async.series({ - parseNoteIdByAlias: function (_callback) { - // try to parse note id by alias (e.g. doc) - Note.findOne({ - where: { - alias: noteId - } - }).then(function (note) { - if (note) { - const filePath = path.join(config.docsPath, noteId + '.md') - if (Note.checkFileExist(filePath)) { - // if doc in filesystem have newer modified time than last change time - // then will update the doc in db - const fsModifiedTime = moment(fs.statSync(filePath).mtime) - const dbModifiedTime = moment(note.lastchangeAt || note.createdAt) - const body = fs.readFileSync(filePath, 'utf8') - const contentLength = body.length - const title = Note.parseNoteTitle(body) - if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { - note.update({ - title: title, - content: body, - lastchangeAt: fsModifiedTime - }).then(function (note) { - Revision.saveNoteRevision(note, function (err, revision) { - if (err) return _callback(err, null) - // update authorship on after making revision of docs - const patch = dmp.patch_fromText(revision.patch) - const operations = Note.transformPatchToOperations(patch, contentLength) - let authorship = note.authorship - for (let i = 0; i < operations.length; i++) { - authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship) - } - note.update({ - authorship: authorship - }).then(function (note) { - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - }) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return callback(null, note.id) - } - } else { - return callback(null, note.id) - } - } else { - const filePath = path.join(config.docsPath, noteId + '.md') - if (Note.checkFileExist(filePath)) { - Note.create({ - alias: noteId, - owner: null, - permission: 'locked' - }).then(function (note) { - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return _callback(null, null) - } - } - }).catch(function (err) { - return _callback(err, null) - }) - }, - // parse note id by LZString is deprecated, here for compability - parseNoteIdByLZString: function (_callback) { - // Calculate minimal string length for an UUID that is encoded - // base64 encoded and optimize comparsion by using -1 - // this should make a lot of LZ-String parsing errors obsolete - // as we can assume that a nodeId that is 48 chars or longer is a - // noteID. - const base64UuidLength = ((4 * 36) / 3) - 1 - if (!(noteId.length > base64UuidLength)) { - return _callback(null, null) - } - // try to parse note id by LZString Base64 - try { - const id = LZString.decompressFromBase64(noteId) - if (id && Note.checkNoteIdValid(id)) { - return callback(null, id) - } else { - return _callback(null, null) - } - } catch (err) { - if (err.message === 'Cannot read property \'charAt\' of undefined') { - logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.') - } else { - logger.error(err) - } - return _callback(null, null) - } - }, - parseNoteIdByBase64Url: function (_callback) { - // try to parse note id by base64url - try { - const id = Note.decodeNoteId(noteId) - if (id && Note.checkNoteIdValid(id)) { - return callback(null, id) - } else { - return _callback(null, null) - } - } catch (err) { - logger.error(err) - return _callback(null, null) - } - }, - parseNoteIdByShortId: function (_callback) { - // try to parse note id by shortId - try { - if (shortIdIsValid(noteId)) { - Note.findOne({ - where: { - shortid: noteId - } - }).then(function (note) { - if (!note) return _callback(null, null) - return callback(null, note.id) - }).catch(function (err) { - return _callback(err, null) - }) - } else { - return _callback(null, null) - } - } catch (err) { - return _callback(err, null) - } - } - }, function (err, _) { - if (err) { - logger.error(err) - return callback(err, null) - } - return callback(null, null) - }) - } - - static parseNoteTitle (body): string { - const parsed = Note.extractMeta(body) - const $ = cheerio.load(md.render(parsed.markdown)) - return Note.extractNoteTitle(parsed.meta, $) - } - - static extractNoteTitle (meta, $): string { - let title = '' - if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { - title = meta.title - } else { - const h1s = $('h1') - if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { - title = S(h1s.first().text()).stripTags().s - } - } - if (!title) title = 'Untitled' - return title - } - - static generateDescription (markdown): string { - return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ') - } - - static decodeTitle (title): string { - return title || 'Untitled' - } - - static generateWebTitle (title): string { - title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD' - return title - } - - static extractNoteTags (meta, $): string[] { - const tags: string[] = [] - const rawtags: string[] = [] - if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) { - const metaTags = ('' + meta.tags).split(',') - for (let i = 0; i < metaTags.length; i++) { - const text: string = metaTags[i].trim() - if (text) rawtags.push(text) - } - } else { - const h6s = $('h6') - h6s.each(function (key, value) { - if (/^tags/gmi.test($(value).text())) { - const codes = $(value).find('code') - for (let i = 0; i < codes.length; i++) { - const text = S($(codes[i]).text().trim()).stripTags().s - if (text) rawtags.push(text) - } - } - }) - } - for (let i = 0; i < rawtags.length; i++) { - let found = false - for (let j = 0; j < tags.length; j++) { - if (tags[j] === rawtags[i]) { - found = true - break - } - } - if (!found) { - tags.push(rawtags[i]) - } - } - return tags - } - - static extractMeta (content): { markdown: string; meta: {} } { - try { - const obj = metaMarked(content) - if (!obj.markdown) obj.markdown = '' - if (!obj.meta) obj.meta = {} - return obj - } catch (err) { - return { - markdown: content, - meta: {} - } - } - } - - static parseMeta (meta): NoteMetadata { - const _meta = new NoteMetadata() - if (meta) { - if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { - _meta.title = meta.title - } - if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { - _meta.description = meta.description - } - if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { - _meta.robots = meta.robots - } - if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { - _meta.GA = meta.GA - } - if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { - _meta.disqus = meta.disqus - } - if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { - _meta.slideOptions = meta.slideOptions - } - if (meta.opengraph && (typeof meta.opengraph === 'object')) { - _meta.opengraph = meta.opengraph - } - } - return _meta - } - - static parseOpengraph (meta, title: string): OpengraphMetadata { - let _ogdata = new OpengraphMetadata() - if (meta.opengraph) { - _ogdata = meta.opengraph - } - if (!(_ogdata.title && (typeof _ogdata.title === 'string' || typeof _ogdata.title === 'number'))) { - _ogdata.title = title - } - if (!(_ogdata.description && (typeof _ogdata.description === 'string' || typeof _ogdata.description === 'number'))) { - _ogdata.description = meta.description || '' - } - if (!(_ogdata.type && (typeof _ogdata.type === 'string'))) { - _ogdata.type = 'website' - } - return _ogdata - } - - static updateAuthorshipByOperation (operation, userId: string | null, authorships): NoteAuthorship[] { - let index = 0 - const timestamp = Date.now() - for (let i = 0; i < operation.length; i++) { - const op = operation[i] - if (ot.TextOperation.isRetain(op)) { - index += op - } else if (ot.TextOperation.isInsert(op)) { - const opStart = index - const opEnd = index + op.length - let inserted = false - // authorship format: [userId, startPos, endPos, createdAt, updatedAt] - if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]) - else { - for (let j = 0; j < authorships.length; j++) { - const authorship = authorships[j] - if (!inserted) { - const nextAuthorship = authorships[j + 1] || -1 - if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) { - if (authorship[1] < opStart && authorship[2] > opStart) { - // divide - const postLength = authorship[2] - opStart - authorship[2] = opStart - authorship[4] = timestamp - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) - authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]) - j += 2 - inserted = true - } else if (authorship[1] >= opStart) { - authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]) - j += 1 - inserted = true - } else if (authorship[2] <= opStart) { - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) - j += 1 - inserted = true - } - } - } - if (authorship[1] >= opStart) { - authorship[1] += op.length - authorship[2] += op.length - } - } - } - index += op.length - } else if (ot.TextOperation.isDelete(op)) { - const opStart = index - const opEnd = index - op - if (operation.length === 1) { - authorships = [] - } else if (authorships.length > 0) { - for (let j = 0; j < authorships.length; j++) { - const authorship = authorships[j] - if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { - authorships.splice(j, 1) - j -= 1 - } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { - authorship[2] += op - authorship[4] = timestamp - } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { - authorship[2] = opStart - authorship[4] = timestamp - } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { - authorship[1] = opEnd - authorship[4] = timestamp - } - if (authorship[1] >= opEnd) { - authorship[1] += op - authorship[2] += op - } - } - } - index += op - } - } - // merge - for (let j = 0; j < authorships.length; j++) { - const authorship = authorships[j] - for (let k = j + 1; k < authorships.length; k++) { - const nextAuthorship = authorships[k] - if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { - const minTimestamp = Math.min(authorship[3], nextAuthorship[3]) - const maxTimestamp = Math.max(authorship[3], nextAuthorship[3]) - authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]) - authorships.splice(k, 1) - j -= 1 - break - } - } - } - // clear - for (let j = 0; j < authorships.length; j++) { - const authorship = authorships[j] - if (!authorship[0]) { - authorships.splice(j, 1) - j -= 1 - } - } - return authorships - } - - // eslint-disable-next-line @typescript-eslint/camelcase - static transformPatchToOperations (patch: patch_obj[], contentLength): number[][] { - const operations: number[][] = [] - if (patch.length > 0) { - // calculate original content length - for (let j = patch.length - 1; j >= 0; j--) { - const p = patch[j] - for (let i = 0; i < p.diffs.length; i++) { - const diff = p.diffs[i] - switch (diff[0]) { - case 1: // insert - contentLength -= diff[1].length - break - case -1: // delete - contentLength += diff[1].length - break - } - } - } - // generate operations - let bias = 0 - let lengthBias = 0 - for (let j = 0; j < patch.length; j++) { - const operation: number[] = [] - const p = patch[j] - let currIndex = p.start1 || 0 - const currLength = contentLength - bias - for (let i = 0; i < p.diffs.length; i++) { - const diff = p.diffs[i] - switch (diff[0]) { - case 0: // retain - if (i === 0) { - // first - operation.push(currIndex + diff[1].length) - } else if (i !== p.diffs.length - 1) { - // mid - operation.push(diff[1].length) - } else { - // last - operation.push(currLength + lengthBias - currIndex) - } - currIndex += diff[1].length - break - case 1: // insert - operation.push(diff[1].length) - lengthBias += diff[1].length - currIndex += diff[1].length - break - case -1: // delete - operation.push(-diff[1].length) - bias += diff[1].length - currIndex += diff[1].length - break - } - } - operations.push(operation) - } - } - return operations - } - - static parseNoteInfo (body): { title: string; tags: string[] } { - const parsed = Note.extractMeta(body) - const $ = cheerio.load(md.render(parsed.markdown)) - return { - title: Note.extractNoteTitle(parsed.meta, $), - tags: Note.extractNoteTags(parsed.meta, $) - } - } -} diff --git a/old_src/lib/models/revision.ts b/old_src/lib/models/revision.ts deleted file mode 100644 index 9bc12c8da..000000000 --- a/old_src/lib/models/revision.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { ChildProcess } from 'child_process' - -import Sequelize from 'sequelize' -import { BelongsTo, Column, DataType, Default, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript' -// core -import { logger } from '../logger' -import { processData, stripNullByte } from '../utils/functions' -import { Note } from './note' -import async = require('async') -import childProcess = require('child_process') -import moment = require('moment') -import path = require('path') -import shortId = require('shortid') - -const Op = Sequelize.Op - -const dmpCallbackCache = {} - -class Data { - msg - cacheKey - error - result - level -} - -function createDmpWorker (): ChildProcess { - const worker = childProcess.fork(path.resolve(__dirname, '../workers/dmpWorker'), ['ignore']) - logger.debug('dmp worker process started') - worker.on('message', function (data: Data) { - if (!data || !data.msg || !data.cacheKey) { - logger.error('dmp worker error: not enough data on message') - return - } - const cacheKey = data.cacheKey - switch (data.msg) { - case 'log': - logger.log(data.level, data.result[0], ...data.result[1]) - // The cacheKey is a dummy value and we want to skip the delete line. - return - case 'error': - dmpCallbackCache[cacheKey](data.error, null) - break - case 'check': - dmpCallbackCache[cacheKey](null, data.result) - break - } - delete dmpCallbackCache[cacheKey] - }) - worker.on('close', function (code) { - logger.debug(`dmp worker process exited with code ${code}`) - }) - return worker -} - -let dmpWorker: ChildProcess = createDmpWorker() - -function sendDmpWorker (data, callback): void { - if (!dmpWorker) { - dmpWorker = createDmpWorker() - } - const cacheKey = Date.now() + '_' + shortId.generate() - dmpCallbackCache[cacheKey] = callback - data = Object.assign(data, { - cacheKey: cacheKey - }) - dmpWorker.send(data) -} - -@Table -export class Revision extends Model { - @Default(Sequelize.UUIDV4) - @PrimaryKey - @Column(DataType.UUID) - id: string - - @Column(DataType.INTEGER) - length: number - - @ForeignKey(() => Note) - @Column(DataType.UUID) - noteId: string - - @BelongsTo(() => Note, { foreignKey: 'noteId', constraints: false, onDelete: 'CASCADE', hooks: true }) - note: Note - - @Column(DataType.TEXT({ length: 'long' })) - get patch (): string { - return this.getDataValue('patch') ?? '' - } - - set patch (value: string) { - this.setDataValue('patch', stripNullByte(value)) - } - - @Column(DataType.TEXT({ length: 'long' })) - get lastContent (): string { - return this.getDataValue('lastContent') ?? '' - } - - set lastContent (value: string) { - this.setDataValue('lastContent', stripNullByte(value)) - } - - @Column(DataType.TEXT({ length: 'long' })) - get content (): string { - return this.getDataValue('content') ?? '' - } - - set content (value: string) { - this.setDataValue('content', stripNullByte(value)) - } - - @Column(DataType.TEXT({ length: 'long' })) - get authorship (): string { - return processData(this.getDataValue('authorship'), [], JSON.parse) - } - - set authorship (value: string) { - this.setDataValue('authorship', value ? JSON.stringify(value) : value) - } - - static getNoteRevisions (note: Note, callback): void { - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions: Revision[]) { - class RevisionDataActions { // TODO: Fix Type in actions.ts - time - - length - } - - const data: RevisionDataActions[] = [] - revisions.forEach(function (revision: Revision) { - data.push({ - time: moment(revision.createdAt).valueOf(), - length: revision.length - }) - }) - callback(null, data) - }).catch(function (err) { - callback(err, null) - }) - } - - static getPatchedNoteRevisionByTime (note: Note, time, errorCallback): void { - // find all revisions to prepare for all possible calculation - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions: Revision[]) { - if (revisions.length <= 0) { - errorCallback(null, null) - return - } - // measure target revision position - Revision.count({ - where: { - noteId: note.id, - createdAt: { - [Op.gte]: time - } - } - }).then(function (count: number) { - if (count <= 0) { - errorCallback(null, null) - return - } - sendDmpWorker({ - msg: 'get revision', - revisions: revisions, - count: count - }, errorCallback) - }).catch(function (err) { - errorCallback(err, null) - }) - }).catch(function (err) { - errorCallback(err, null) - }) - } - - static checkAllNotesRevision (callback): void { - Revision.saveAllNotesRevision(function (err, notes: Note[]) { - if (err) { - callback(err, null) - return - } - if (!notes || notes.length <= 0) { - callback(null, notes) - } else { - Revision.checkAllNotesRevision(callback) - } - }) - } - - static saveAllNotesRevision (callback): void { - Note.findAll({ - // query all notes that need to save for revision - where: { - [Op.and]: [ - { - lastchangeAt: { - [Op.or]: { - [Op.eq]: null, - [Op.and]: { - [Op.ne]: null, - [Op.gt]: Sequelize.col('createdAt') - } - } - } - }, - { - savedAt: { - [Op.or]: { - [Op.eq]: null, - [Op.lt]: Sequelize.col('lastchangeAt') - } - } - } - ] - } - }).then(function (notes: Note[]) { - if (notes.length <= 0) { - callback(null, notes) - return - } - const savedNotes: Note[] = [] - async.each(notes, function (note: Note, _callback) { - // revision saving policy: note not been modified for 5 mins or not save for 10 mins - if (note.lastchangeAt && note.savedAt) { - const lastchangeAt = moment(note.lastchangeAt) - const savedAt = moment(note.savedAt) - if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } else { - _callback(null, null) - } - } else { - savedNotes.push(note) - Revision.saveNoteRevision(note, _callback) - } - }, function (err) { - if (err) { - callback(err, null) - return - } - // return null when no notes need saving at this moment but have delayed tasks to be done - const result = ((savedNotes.length === 0) && (notes.length > 0)) ? null : savedNotes - callback(null, result) - }) - }).catch(function (err) { - callback(err, null) - }) - } - - static saveNoteRevision (note: Note, callback): void { - Revision.findAll({ - where: { - noteId: note.id - }, - order: [['createdAt', 'DESC']] - }).then(function (revisions: Revision[]) { - if (revisions.length <= 0) { - // if no revision available - let noteContent = note.content - if (noteContent.length === 0) { - noteContent = '' - } - Revision.create({ - noteId: note.id, - lastContent: noteContent, - length: noteContent.length, - authorship: note.authorship - }).then(function (revision: Revision) { - Revision.finishSaveNoteRevision(note, revision, callback) - }).catch(function (err) { - callback(err, null) - }) - } else { - const latestRevision = revisions[0] - const lastContent = latestRevision.content || latestRevision.lastContent - const content = note.content - sendDmpWorker({ - msg: 'create patch', - lastDoc: lastContent, - currDoc: content - }, function (err, patch) { - if (err) { - logger.error('save note revision error', err) - return - } - if (!patch) { - // if patch is empty (means no difference) then just update the latest revision updated time - latestRevision.changed('updatedAt', true) - latestRevision.update({ - updatedAt: Date.now() - }).then(function (revision: Revision) { - Revision.finishSaveNoteRevision(note, revision, callback) - }).catch(function (err) { - callback(err, null) - }) - } else { - Revision.create({ - noteId: note.id, - patch: patch, - content: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision: Revision) { - // clear last revision content to reduce db size - latestRevision.update({ - content: null - }).then(function () { - Revision.finishSaveNoteRevision(note, revision, callback) - }).catch(function (err) { - callback(err, null) - }) - }).catch(function (err) { - callback(err, null) - }) - } - }) - } - }).catch(function (err) { - callback(err, null) - }) - } - - static finishSaveNoteRevision (note: Note, revision: Revision, callback): void { - note.update({ - savedAt: revision.updatedAt - }).then(function () { - callback(null, revision) - }).catch(function (err) { - callback(err, null) - }) - } -} diff --git a/old_src/lib/models/temp.ts b/old_src/lib/models/temp.ts deleted file mode 100644 index c6a64ebc8..000000000 --- a/old_src/lib/models/temp.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { DataType, Model, Table, PrimaryKey, Column, Default } from 'sequelize-typescript' -import { generate as shortIdGenerate } from 'shortid' - -@Table -export class Temp extends Model { - @Default(shortIdGenerate) - @PrimaryKey - @Column(DataType.STRING) - id: string; - - @Column(DataType.TEXT) - data: string -} diff --git a/old_src/lib/models/user.ts b/old_src/lib/models/user.ts deleted file mode 100644 index aecfbb740..000000000 --- a/old_src/lib/models/user.ts +++ /dev/null @@ -1,74 +0,0 @@ -import scrypt from 'scrypt-kdf' -import { UUIDV4 } from 'sequelize' -import { - BeforeCreate, - BeforeUpdate, - Column, - DataType, - Default, - HasMany, - IsEmail, - Model, - PrimaryKey, - Table, - Unique -} from 'sequelize-typescript' -import { Note } from './note' - -@Table -export class User extends Model { - @PrimaryKey - @Default(UUIDV4) - @Column(DataType.UUID) - id: string - - @Unique - @Column(DataType.STRING) - profileid: string - - @Column(DataType.TEXT) - profile: string - - @Column(DataType.TEXT) - history: string - - @Column(DataType.TEXT) - accessToken: string - - @Column(DataType.TEXT) - refreshToken: string - - @Column(DataType.UUID) - deleteToken: string - - @IsEmail - @Column(DataType.TEXT) - email: string - - @Column(DataType.TEXT) - password: string - - @HasMany(() => Note, { foreignKey: 'lastchangeuserId', constraints: false }) - @HasMany(() => Note, { foreignKey: 'ownerId', constraints: false }) - - @BeforeUpdate - @BeforeCreate - static async updatePasswordHashHook (user: User): Promise { - // suggested way to hash passwords to be able to do this asynchronously: - // @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819 - - if (!user.changed('password')) { - return Promise.resolve() - } - - return scrypt - .kdf(user.getDataValue('password'), { logN: 15, r: 8, p: 1 }) - .then(keyBuf => { - user.setDataValue('password', keyBuf.toString('hex')) - }) - } - - verifyPassword (attempt: string): Promise { - return scrypt.verify(Buffer.from(this.password, 'hex'), attempt) - } -} diff --git a/old_src/lib/ot/client.js b/old_src/lib/ot/client.js deleted file mode 100755 index 7ee19fdc2..000000000 --- a/old_src/lib/ot/client.js +++ /dev/null @@ -1,312 +0,0 @@ -// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala - -if (typeof ot === 'undefined') { - var ot = {}; -} - -ot.Client = (function (global) { - 'use strict'; - - // Client constructor - function Client (revision) { - this.revision = revision; // the next expected revision number - this.setState(synchronized_); // start state - } - - Client.prototype.setState = function (state) { - this.state = state; - }; - - // Call this method when the user changes the document. - Client.prototype.applyClient = function (operation) { - this.setState(this.state.applyClient(this, operation)); - }; - - // Call this method with a new operation from the server - Client.prototype.applyServer = function (revision, operation) { - this.setState(this.state.applyServer(this, revision, operation)); - }; - - Client.prototype.applyOperations = function (head, operations) { - this.setState(this.state.applyOperations(this, head, operations)); - }; - - Client.prototype.serverAck = function (revision) { - this.setState(this.state.serverAck(this, revision)); - }; - - Client.prototype.serverReconnect = function () { - if (typeof this.state.resend === 'function') { this.state.resend(this); } - }; - - // Transforms a selection from the latest known server state to the current - // client state. For example, if we get from the server the information that - // another user's cursor is at position 3, but the server hasn't yet received - // our newest operation, an insertion of 5 characters at the beginning of the - // document, the correct position of the other user's cursor in our current - // document is 8. - Client.prototype.transformSelection = function (selection) { - return this.state.transformSelection(selection); - }; - - // Override this method. - Client.prototype.sendOperation = function (revision, operation) { - throw new Error("sendOperation must be defined in child class"); - }; - - // Override this method. - Client.prototype.applyOperation = function (operation) { - throw new Error("applyOperation must be defined in child class"); - }; - - - // In the 'Synchronized' state, there is no pending operation that the client - // has sent to the server. - function Synchronized () {} - Client.Synchronized = Synchronized; - - Synchronized.prototype.applyClient = function (client, operation) { - // When the user makes an edit, send the operation to the server and - // switch to the 'AwaitingConfirm' state - client.sendOperation(client.revision, operation); - return new AwaitingConfirm(operation); - }; - - Synchronized.prototype.applyServer = function (client, revision, operation) { - if (revision - client.revision > 1) { - throw new Error("Invalid revision."); - } - client.revision = revision; - // When we receive a new operation from the server, the operation can be - // simply applied to the current document - client.applyOperation(operation); - return this; - }; - - Synchronized.prototype.serverAck = function (client, revision) { - throw new Error("There is no pending operation."); - }; - - // Nothing to do because the latest server state and client state are the same. - Synchronized.prototype.transformSelection = function (x) { return x; }; - - // Singleton - var synchronized_ = new Synchronized(); - - - // In the 'AwaitingConfirm' state, there's one operation the client has sent - // to the server and is still waiting for an acknowledgement. - function AwaitingConfirm (outstanding) { - // Save the pending operation - this.outstanding = outstanding; - } - Client.AwaitingConfirm = AwaitingConfirm; - - AwaitingConfirm.prototype.applyClient = function (client, operation) { - // When the user makes an edit, don't send the operation immediately, - // instead switch to 'AwaitingWithBuffer' state - return new AwaitingWithBuffer(this.outstanding, operation); - }; - - AwaitingConfirm.prototype.applyServer = function (client, revision, operation) { - if (revision - client.revision > 1) { - throw new Error("Invalid revision."); - } - client.revision = revision; - // This is another client's operation. Visualization: - // - // /\ - // this.outstanding / \ operation - // / \ - // \ / - // pair[1] \ / pair[0] (new outstanding) - // (can be applied \/ - // to the client's - // current document) - var pair = operation.constructor.transform(this.outstanding, operation); - client.applyOperation(pair[1]); - return new AwaitingConfirm(pair[0]); - }; - - AwaitingConfirm.prototype.serverAck = function (client, revision) { - if (revision - client.revision > 1) { - return new Stale(this.outstanding, client, revision).getOperations(); - } - client.revision = revision; - // The client's operation has been acknowledged - // => switch to synchronized state - return synchronized_; - }; - - AwaitingConfirm.prototype.transformSelection = function (selection) { - return selection.transform(this.outstanding); - }; - - AwaitingConfirm.prototype.resend = function (client) { - // The confirm didn't come because the client was disconnected. - // Now that it has reconnected, we resend the outstanding operation. - client.sendOperation(client.revision, this.outstanding); - }; - - - // In the 'AwaitingWithBuffer' state, the client is waiting for an operation - // to be acknowledged by the server while buffering the edits the user makes - function AwaitingWithBuffer (outstanding, buffer) { - // Save the pending operation and the user's edits since then - this.outstanding = outstanding; - this.buffer = buffer; - } - Client.AwaitingWithBuffer = AwaitingWithBuffer; - - AwaitingWithBuffer.prototype.applyClient = function (client, operation) { - // Compose the user's changes onto the buffer - var newBuffer = this.buffer.compose(operation); - return new AwaitingWithBuffer(this.outstanding, newBuffer); - }; - - AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) { - if (revision - client.revision > 1) { - throw new Error("Invalid revision."); - } - client.revision = revision; - // Operation comes from another client - // - // /\ - // this.outstanding / \ operation - // / \ - // /\ / - // this.buffer / \* / pair1[0] (new outstanding) - // / \/ - // \ / - // pair2[1] \ / pair2[0] (new buffer) - // the transformed \/ - // operation -- can - // be applied to the - // client's current - // document - // - // * pair1[1] - var transform = operation.constructor.transform; - var pair1 = transform(this.outstanding, operation); - var pair2 = transform(this.buffer, pair1[1]); - client.applyOperation(pair2[1]); - return new AwaitingWithBuffer(pair1[0], pair2[0]); - }; - - AwaitingWithBuffer.prototype.serverAck = function (client, revision) { - if (revision - client.revision > 1) { - return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations(); - } - client.revision = revision; - // The pending operation has been acknowledged - // => send buffer - client.sendOperation(client.revision, this.buffer); - return new AwaitingConfirm(this.buffer); - }; - - AwaitingWithBuffer.prototype.transformSelection = function (selection) { - return selection.transform(this.outstanding).transform(this.buffer); - }; - - AwaitingWithBuffer.prototype.resend = function (client) { - // The confirm didn't come because the client was disconnected. - // Now that it has reconnected, we resend the outstanding operation. - client.sendOperation(client.revision, this.outstanding); - }; - - - function Stale(acknowlaged, client, revision) { - this.acknowlaged = acknowlaged; - this.client = client; - this.revision = revision; - } - Client.Stale = Stale; - - Stale.prototype.applyClient = function (client, operation) { - return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision); - }; - - Stale.prototype.applyServer = function (client, revision, operation) { - throw new Error("Ignored server-side change."); - }; - - Stale.prototype.applyOperations = function (client, head, operations) { - var transform = this.acknowlaged.constructor.transform; - for (var i = 0; i < operations.length; i++) { - var op = ot.TextOperation.fromJSON(operations[i]); - var pair = transform(this.acknowlaged, op); - client.applyOperation(pair[1]); - this.acknowlaged = pair[0]; - } - client.revision = this.revision; - return synchronized_; - }; - - Stale.prototype.serverAck = function (client, revision) { - throw new Error("There is no pending operation."); - }; - - Stale.prototype.transformSelection = function (selection) { - return selection; - }; - - Stale.prototype.getOperations = function () { - this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision - return this; - }; - - - function StaleWithBuffer(acknowlaged, buffer, client, revision) { - this.acknowlaged = acknowlaged; - this.buffer = buffer; - this.client = client; - this.revision = revision; - } - Client.StaleWithBuffer = StaleWithBuffer; - - StaleWithBuffer.prototype.applyClient = function (client, operation) { - var buffer = this.buffer.compose(operation); - return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision); - }; - - StaleWithBuffer.prototype.applyServer = function (client, revision, operation) { - throw new Error("Ignored server-side change."); - }; - - StaleWithBuffer.prototype.applyOperations = function (client, head, operations) { - var transform = this.acknowlaged.constructor.transform; - for (var i = 0; i < operations.length; i++) { - var op = ot.TextOperation.fromJSON(operations[i]); - var pair1 = transform(this.acknowlaged, op); - var pair2 = transform(this.buffer, pair1[1]); - client.applyOperation(pair2[1]); - this.acknowlaged = pair1[0]; - this.buffer = pair2[0]; - } - client.revision = this.revision; - client.sendOperation(client.revision, this.buffer); - return new AwaitingConfirm(this.buffer); - }; - - StaleWithBuffer.prototype.serverAck = function (client, revision) { - throw new Error("There is no pending operation."); - }; - - StaleWithBuffer.prototype.transformSelection = function (selection) { - return selection; - }; - - StaleWithBuffer.prototype.getOperations = function () { - this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision - return this; - }; - - - return Client; - -}(this)); - -if (typeof module === 'object') { - module.exports = ot.Client; -} - diff --git a/old_src/lib/ot/editor-socketio-server.ts b/old_src/lib/ot/editor-socketio-server.ts deleted file mode 100755 index 5cf7d27b0..000000000 --- a/old_src/lib/ot/editor-socketio-server.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { EventEmitter } from 'events' -import { logger } from '../logger' -import { SocketWithNoteId } from '../realtime' -import Selection from './selection' -import Server from './server' -import TextOperation from './text-operation' -import WrappedOperation from './wrapped-operation' - -export class EditorSocketIOServer extends Server { - private readonly users: {} - private readonly docId: any - private mayWrite: (socket: SocketWithNoteId, originIsOperation: boolean, callback: (mayEdit: boolean) => void) => void - - constructor (document, operations, docId, mayWrite, operationCallback) { - super(document, operations) - // Whatever that does? - EventEmitter.call(this) - this.users = {} - this.docId = docId - this.mayWrite = mayWrite || function (_, originIsOperation, cb) { - cb(true) - } - this.operationCallback = operationCallback - } - - addClient (socket) { - const self = this - socket.join(this.docId) - const docOut = { - str: this.document, - revision: this.operations.length, - clients: this.users - } - socket.emit('doc', docOut) - socket.on('operation', function (revision, operation, selection) { - self.mayWrite(socket, true, function (mayWrite) { - if (!mayWrite) { - logger.info("User doesn't have the right to edit.") - return - } - try { - self.onOperation(socket, revision, operation, selection) - if (typeof self.operationCallback === 'function') - self.operationCallback(socket, operation) - } catch (err) { - setTimeout(function () { - const docOut = { - str: self.document, - revision: self.operations.length, - clients: self.users, - force: true - } - socket.emit('doc', docOut) - }, 100) - } - }) - }) - socket.on('get_operations', function (base, head) { - self.onGetOperations(socket, base, head) - }) - socket.on('selection', function (obj) { - self.mayWrite(socket, false, function (mayWrite) { - if (!mayWrite) { - logger.info("User doesn't have the right to edit.") - return - } - self.updateSelection(socket, obj && Selection.fromJSON(obj)) - }) - }) - socket.on('disconnect', function () { - logger.debug("Disconnect") - socket.leave(self.docId) - self.onDisconnect(socket) - /* - if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) { - self.emit('empty-room'); - } - */ - }) - }; - - onOperation (socket, revision, operation, selection) { - let wrapped - try { - wrapped = new WrappedOperation( - TextOperation.fromJSON(operation), - selection && Selection.fromJSON(selection) - ) - } catch (exc) { - logger.error("Invalid operation received: ") - logger.error(exc) - throw new Error(exc) - } - - try { - const clientId = socket.id - const wrappedPrime = this.receiveOperation(revision, wrapped) - if (!wrappedPrime) return - logger.debug("new operation: " + JSON.stringify(wrapped)) - this.getClient(clientId).selection = wrappedPrime.meta - revision = this.operations.length - socket.emit('ack', revision) - socket.broadcast.in(this.docId).emit( - 'operation', clientId, revision, - wrappedPrime.wrapped.toJSON(), wrappedPrime.meta - ) - //set document is dirty - this.isDirty = true - } catch (exc) { - logger.error(exc) - throw new Error(exc) - } - }; - - onGetOperations (socket, base, head) { - const operations = this.operations.slice(base, head).map(function (op) { - return op.wrapped.toJSON() - }) - socket.emit('operations', head, operations) - }; - - updateSelection (socket, selection) { - const clientId = socket.id - if (selection) { - this.getClient(clientId).selection = selection - } else { - delete this.getClient(clientId).selection - } - socket.broadcast.to(this.docId).emit('selection', clientId, selection) - }; - - setName (socket, name) { - const clientId = socket.id - this.getClient(clientId).name = name - socket.broadcast.to(this.docId).emit('set_name', clientId, name) - }; - - setColor (socket, color) { - const clientId = socket.id - this.getClient(clientId).color = color - socket.broadcast.to(this.docId).emit('set_color', clientId, color) - }; - - getClient (clientId) { - return this.users[clientId] || (this.users[clientId] = {}) - }; - - onDisconnect (socket) { - const clientId = socket.id - delete this.users[clientId] - socket.broadcast.to(this.docId).emit('client_left', clientId) - }; -} - diff --git a/old_src/lib/ot/index.js b/old_src/lib/ot/index.js deleted file mode 100644 index fcc94c11b..000000000 --- a/old_src/lib/ot/index.js +++ /dev/null @@ -1,8 +0,0 @@ -exports.version = '0.0.15'; - -exports.TextOperation = require('./text-operation'); -exports.SimpleTextOperation = require('./simple-text-operation'); -exports.Client = require('./client'); -exports.Server = require('./server'); -exports.Selection = require('./selection'); -exports.EditorSocketIOServer = require('./editor-socketio-server'); diff --git a/old_src/lib/ot/selection.js b/old_src/lib/ot/selection.js deleted file mode 100644 index 72bf8bd65..000000000 --- a/old_src/lib/ot/selection.js +++ /dev/null @@ -1,117 +0,0 @@ -if (typeof ot === 'undefined') { - // Export for browsers - var ot = {}; -} - -ot.Selection = (function (global) { - 'use strict'; - - var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation'); - - // Range has `anchor` and `head` properties, which are zero-based indices into - // the document. The `anchor` is the side of the selection that stays fixed, - // `head` is the side of the selection where the cursor is. When both are - // equal, the range represents a cursor. - function Range (anchor, head) { - this.anchor = anchor; - this.head = head; - } - - Range.fromJSON = function (obj) { - return new Range(obj.anchor, obj.head); - }; - - Range.prototype.equals = function (other) { - return this.anchor === other.anchor && this.head === other.head; - }; - - Range.prototype.isEmpty = function () { - return this.anchor === this.head; - }; - - Range.prototype.transform = function (other) { - function transformIndex (index) { - var newIndex = index; - var ops = other.ops; - for (var i = 0, l = other.ops.length; i < l; i++) { - if (TextOperation.isRetain(ops[i])) { - index -= ops[i]; - } else if (TextOperation.isInsert(ops[i])) { - newIndex += ops[i].length; - } else { - newIndex -= Math.min(index, -ops[i]); - index += ops[i]; - } - if (index < 0) { break; } - } - return newIndex; - } - - var newAnchor = transformIndex(this.anchor); - if (this.anchor === this.head) { - return new Range(newAnchor, newAnchor); - } - return new Range(newAnchor, transformIndex(this.head)); - }; - - // A selection is basically an array of ranges. Every range represents a real - // selection or a cursor in the document (when the start position equals the - // end position of the range). The array must not be empty. - function Selection (ranges) { - this.ranges = ranges || []; - } - - Selection.Range = Range; - - // Convenience method for creating selections only containing a single cursor - // and no real selection range. - Selection.createCursor = function (position) { - return new Selection([new Range(position, position)]); - }; - - Selection.fromJSON = function (obj) { - var objRanges = obj.ranges || obj; - for (var i = 0, ranges = []; i < objRanges.length; i++) { - ranges[i] = Range.fromJSON(objRanges[i]); - } - return new Selection(ranges); - }; - - Selection.prototype.equals = function (other) { - if (this.position !== other.position) { return false; } - if (this.ranges.length !== other.ranges.length) { return false; } - // FIXME: Sort ranges before comparing them? - for (var i = 0; i < this.ranges.length; i++) { - if (!this.ranges[i].equals(other.ranges[i])) { return false; } - } - return true; - }; - - Selection.prototype.somethingSelected = function () { - for (var i = 0; i < this.ranges.length; i++) { - if (!this.ranges[i].isEmpty()) { return true; } - } - return false; - }; - - // Return the more current selection information. - Selection.prototype.compose = function (other) { - return other; - }; - - // Update the selection with respect to an operation. - Selection.prototype.transform = function (other) { - for (var i = 0, newRanges = []; i < this.ranges.length; i++) { - newRanges[i] = this.ranges[i].transform(other); - } - return new Selection(newRanges); - }; - - return Selection; - -}(this)); - -// Export for CommonJS -if (typeof module === 'object') { - module.exports = ot.Selection; -} diff --git a/old_src/lib/ot/server.js b/old_src/lib/ot/server.js deleted file mode 100644 index 30ebeac96..000000000 --- a/old_src/lib/ot/server.js +++ /dev/null @@ -1,52 +0,0 @@ -var config = require('../config'); - -if (typeof ot === 'undefined') { - var ot = {}; -} - -ot.Server = (function (global) { - 'use strict'; - - // Constructor. Takes the current document as a string and optionally the array - // of all operations. - function Server (document, operations) { - this.document = document; - this.operations = operations || []; - } - - // Call this method whenever you receive an operation from a client. - Server.prototype.receiveOperation = function (revision, operation) { - if (revision < 0 || this.operations.length < revision) { - throw new Error("operation revision not in history"); - } - // Find all operations that the client didn't know of when it sent the - // operation ... - var concurrentOperations = this.operations.slice(revision); - - // ... and transform the operation against all these operations ... - var transform = operation.constructor.transform; - for (var i = 0; i < concurrentOperations.length; i++) { - operation = transform(operation, concurrentOperations[i])[0]; - } - - // ... and apply that on the document. - var newDocument = operation.apply(this.document); - // ignore if exceed the max length of document - if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length) - return; - this.document = newDocument; - // Store operation in history. - this.operations.push(operation); - - // It's the caller's responsibility to send the operation to all connected - // clients and an acknowledgement to the creator. - return operation; - }; - - return Server; - -}(this)); - -if (typeof module === 'object') { - module.exports = ot.Server; -} \ No newline at end of file diff --git a/old_src/lib/ot/simple-text-operation.js b/old_src/lib/ot/simple-text-operation.js deleted file mode 100644 index 6db296e02..000000000 --- a/old_src/lib/ot/simple-text-operation.js +++ /dev/null @@ -1,188 +0,0 @@ -if (typeof ot === 'undefined') { - // Export for browsers - var ot = {}; -} - -ot.SimpleTextOperation = (function (global) { - - var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation'); - - function SimpleTextOperation () {} - - - // Insert the string `str` at the zero-based `position` in the document. - function Insert (str, position) { - if (!this || this.constructor !== SimpleTextOperation) { - // => function was called without 'new' - return new Insert(str, position); - } - this.str = str; - this.position = position; - } - - Insert.prototype = new SimpleTextOperation(); - SimpleTextOperation.Insert = Insert; - - Insert.prototype.toString = function () { - return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')'; - }; - - Insert.prototype.equals = function (other) { - return other instanceof Insert && - this.str === other.str && - this.position === other.position; - }; - - Insert.prototype.apply = function (doc) { - return doc.slice(0, this.position) + this.str + doc.slice(this.position); - }; - - - // Delete `count` many characters at the zero-based `position` in the document. - function Delete (count, position) { - if (!this || this.constructor !== SimpleTextOperation) { - return new Delete(count, position); - } - this.count = count; - this.position = position; - } - - Delete.prototype = new SimpleTextOperation(); - SimpleTextOperation.Delete = Delete; - - Delete.prototype.toString = function () { - return 'Delete(' + this.count + ', ' + this.position + ')'; - }; - - Delete.prototype.equals = function (other) { - return other instanceof Delete && - this.count === other.count && - this.position === other.position; - }; - - Delete.prototype.apply = function (doc) { - return doc.slice(0, this.position) + doc.slice(this.position + this.count); - }; - - - // An operation that does nothing. This is needed for the result of the - // transformation of two deletions of the same character. - function Noop () { - if (!this || this.constructor !== SimpleTextOperation) { return new Noop(); } - } - - Noop.prototype = new SimpleTextOperation(); - SimpleTextOperation.Noop = Noop; - - Noop.prototype.toString = function () { - return 'Noop()'; - }; - - Noop.prototype.equals = function (other) { return other instanceof Noop; }; - - Noop.prototype.apply = function (doc) { return doc; }; - - var noop = new Noop(); - - - SimpleTextOperation.transform = function (a, b) { - if (a instanceof Noop || b instanceof Noop) { return [a, b]; } - - if (a instanceof Insert && b instanceof Insert) { - if (a.position < b.position || (a.position === b.position && a.str < b.str)) { - return [a, new Insert(b.str, b.position + a.str.length)]; - } - if (a.position > b.position || (a.position === b.position && a.str > b.str)) { - return [new Insert(a.str, a.position + b.str.length), b]; - } - return [noop, noop]; - } - - if (a instanceof Insert && b instanceof Delete) { - if (a.position <= b.position) { - return [a, new Delete(b.count, b.position + a.str.length)]; - } - if (a.position >= b.position + b.count) { - return [new Insert(a.str, a.position - b.count), b]; - } - // Here, we have to delete the inserted string of operation a. - // That doesn't preserve the intention of operation a, but it's the only - // thing we can do to get a valid transform function. - return [noop, new Delete(b.count + a.str.length, b.position)]; - } - - if (a instanceof Delete && b instanceof Insert) { - if (a.position >= b.position) { - return [new Delete(a.count, a.position + b.str.length), b]; - } - if (a.position + a.count <= b.position) { - return [a, new Insert(b.str, b.position - a.count)]; - } - // Same problem as above. We have to delete the string that was inserted - // in operation b. - return [new Delete(a.count + b.str.length, a.position), noop]; - } - - if (a instanceof Delete && b instanceof Delete) { - if (a.position === b.position) { - if (a.count === b.count) { - return [noop, noop]; - } else if (a.count < b.count) { - return [noop, new Delete(b.count - a.count, b.position)]; - } - return [new Delete(a.count - b.count, a.position), noop]; - } - if (a.position < b.position) { - if (a.position + a.count <= b.position) { - return [a, new Delete(b.count, b.position - a.count)]; - } - if (a.position + a.count >= b.position + b.count) { - return [new Delete(a.count - b.count, a.position), noop]; - } - return [ - new Delete(b.position - a.position, a.position), - new Delete(b.position + b.count - (a.position + a.count), a.position) - ]; - } - if (a.position > b.position) { - if (a.position >= b.position + b.count) { - return [new Delete(a.count, a.position - b.count), b]; - } - if (a.position + a.count <= b.position + b.count) { - return [noop, new Delete(b.count - a.count, b.position)]; - } - return [ - new Delete(a.position + a.count - (b.position + b.count), b.position), - new Delete(a.position - b.position, b.position) - ]; - } - } - }; - - // Convert a normal, composable `TextOperation` into an array of - // `SimpleTextOperation`s. - SimpleTextOperation.fromTextOperation = function (operation) { - var simpleOperations = []; - var index = 0; - for (var i = 0; i < operation.ops.length; i++) { - var op = operation.ops[i]; - if (TextOperation.isRetain(op)) { - index += op; - } else if (TextOperation.isInsert(op)) { - simpleOperations.push(new Insert(op, index)); - index += op.length; - } else { - simpleOperations.push(new Delete(Math.abs(op), index)); - } - } - return simpleOperations; - }; - - - return SimpleTextOperation; -})(this); - -// Export for CommonJS -if (typeof module === 'object') { - module.exports = ot.SimpleTextOperation; -} \ No newline at end of file diff --git a/old_src/lib/ot/text-operation.js b/old_src/lib/ot/text-operation.js deleted file mode 100644 index d5468497e..000000000 --- a/old_src/lib/ot/text-operation.js +++ /dev/null @@ -1,530 +0,0 @@ -if (typeof ot === 'undefined') { - // Export for browsers - var ot = {}; -} - -ot.TextOperation = (function () { - 'use strict'; - - // Constructor for new operations. - function TextOperation () { - if (!this || this.constructor !== TextOperation) { - // => function was called without 'new' - return new TextOperation(); - } - - // When an operation is applied to an input string, you can think of this as - // if an imaginary cursor runs over the entire string and skips over some - // parts, deletes some parts and inserts characters at some positions. These - // actions (skip/delete/insert) are stored as an array in the "ops" property. - this.ops = []; - // An operation's baseLength is the length of every string the operation - // can be applied to. - this.baseLength = 0; - // The targetLength is the length of every string that results from applying - // the operation on a valid input string. - this.targetLength = 0; - } - - TextOperation.prototype.equals = function (other) { - if (this.baseLength !== other.baseLength) { return false; } - if (this.targetLength !== other.targetLength) { return false; } - if (this.ops.length !== other.ops.length) { return false; } - for (var i = 0; i < this.ops.length; i++) { - if (this.ops[i] !== other.ops[i]) { return false; } - } - return true; - }; - - // Operation are essentially lists of ops. There are three types of ops: - // - // * Retain ops: Advance the cursor position by a given number of characters. - // Represented by positive ints. - // * Insert ops: Insert a given string at the current cursor position. - // Represented by strings. - // * Delete ops: Delete the next n characters. Represented by negative ints. - - var isRetain = TextOperation.isRetain = function (op) { - return typeof op === 'number' && op > 0; - }; - - var isInsert = TextOperation.isInsert = function (op) { - return typeof op === 'string'; - }; - - var isDelete = TextOperation.isDelete = function (op) { - return typeof op === 'number' && op < 0; - }; - - - // After an operation is constructed, the user of the library can specify the - // actions of an operation (skip/insert/delete) with these three builder - // methods. They all return the operation for convenient chaining. - - // Skip over a given number of characters. - TextOperation.prototype.retain = function (n) { - if (typeof n !== 'number') { - throw new Error("retain expects an integer"); - } - if (n === 0) { return this; } - this.baseLength += n; - this.targetLength += n; - if (isRetain(this.ops[this.ops.length-1])) { - // The last op is a retain op => we can merge them into one op. - this.ops[this.ops.length-1] += n; - } else { - // Create a new op. - this.ops.push(n); - } - return this; - }; - - // Insert a string at the current position. - TextOperation.prototype.insert = function (str) { - if (typeof str !== 'string') { - throw new Error("insert expects a string"); - } - if (str === '') { return this; } - this.targetLength += str.length; - var ops = this.ops; - if (isInsert(ops[ops.length-1])) { - // Merge insert op. - ops[ops.length-1] += str; - } else if (isDelete(ops[ops.length-1])) { - // It doesn't matter when an operation is applied whether the operation - // is delete(3), insert("something") or insert("something"), delete(3). - // Here we enforce that in this case, the insert op always comes first. - // This makes all operations that have the same effect when applied to - // a document of the right length equal in respect to the `equals` method. - if (isInsert(ops[ops.length-2])) { - ops[ops.length-2] += str; - } else { - ops[ops.length] = ops[ops.length-1]; - ops[ops.length-2] = str; - } - } else { - ops.push(str); - } - return this; - }; - - // Delete a string at the current position. - TextOperation.prototype['delete'] = function (n) { - if (typeof n === 'string') { n = n.length; } - if (typeof n !== 'number') { - throw new Error("delete expects an integer or a string"); - } - if (n === 0) { return this; } - if (n > 0) { n = -n; } - this.baseLength -= n; - if (isDelete(this.ops[this.ops.length-1])) { - this.ops[this.ops.length-1] += n; - } else { - this.ops.push(n); - } - return this; - }; - - // Tests whether this operation has no effect. - TextOperation.prototype.isNoop = function () { - return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0])); - }; - - // Pretty printing. - TextOperation.prototype.toString = function () { - // map: build a new array by applying a function to every element in an old - // array. - var map = Array.prototype.map || function (fn) { - var arr = this; - var newArr = []; - for (var i = 0, l = arr.length; i < l; i++) { - newArr[i] = fn(arr[i]); - } - return newArr; - }; - return map.call(this.ops, function (op) { - if (isRetain(op)) { - return "retain " + op; - } else if (isInsert(op)) { - return "insert '" + op + "'"; - } else { - return "delete " + (-op); - } - }).join(', '); - }; - - // Converts operation into a JSON value. - TextOperation.prototype.toJSON = function () { - return this.ops; - }; - - // Converts a plain JS object into an operation and validates it. - TextOperation.fromJSON = function (ops) { - var o = new TextOperation(); - for (var i = 0, l = ops.length; i < l; i++) { - var op = ops[i]; - if (isRetain(op)) { - o.retain(op); - } else if (isInsert(op)) { - o.insert(op); - } else if (isDelete(op)) { - o['delete'](op); - } else { - throw new Error("unknown operation: " + JSON.stringify(op)); - } - } - return o; - }; - - // Apply an operation to a string, returning a new string. Throws an error if - // there's a mismatch between the input string and the operation. - TextOperation.prototype.apply = function (str) { - var operation = this; - if (str.length !== operation.baseLength) { - throw new Error("The operation's base length must be equal to the string's length."); - } - var newStr = [], j = 0; - var strIndex = 0; - var ops = this.ops; - for (var i = 0, l = ops.length; i < l; i++) { - var op = ops[i]; - if (isRetain(op)) { - if (strIndex + op > str.length) { - throw new Error("Operation can't retain more characters than are left in the string."); - } - // Copy skipped part of the old string. - newStr[j++] = str.slice(strIndex, strIndex + op); - strIndex += op; - } else if (isInsert(op)) { - // Insert string. - newStr[j++] = op; - } else { // delete op - strIndex -= op; - } - } - if (strIndex !== str.length) { - throw new Error("The operation didn't operate on the whole string."); - } - return newStr.join(''); - }; - - // Computes the inverse of an operation. The inverse of an operation is the - // operation that reverts the effects of the operation, e.g. when you have an - // operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello "); - // skip(6);'. The inverse should be used for implementing undo. - TextOperation.prototype.invert = function (str) { - var strIndex = 0; - var inverse = new TextOperation(); - var ops = this.ops; - for (var i = 0, l = ops.length; i < l; i++) { - var op = ops[i]; - if (isRetain(op)) { - inverse.retain(op); - strIndex += op; - } else if (isInsert(op)) { - inverse['delete'](op.length); - } else { // delete op - inverse.insert(str.slice(strIndex, strIndex - op)); - strIndex -= op; - } - } - return inverse; - }; - - // Compose merges two consecutive operations into one operation, that - // preserves the changes of both. Or, in other words, for each input string S - // and a pair of consecutive operations A and B, - // apply(apply(S, A), B) = apply(S, compose(A, B)) must hold. - TextOperation.prototype.compose = function (operation2) { - var operation1 = this; - if (operation1.targetLength !== operation2.baseLength) { - throw new Error("The base length of the second operation has to be the target length of the first operation"); - } - - var operation = new TextOperation(); // the combined operation - var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access - var i1 = 0, i2 = 0; // current index into ops1 respectively ops2 - var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops - while (true) { - // Dispatch on the type of op1 and op2 - if (typeof op1 === 'undefined' && typeof op2 === 'undefined') { - // end condition: both ops1 and ops2 have been processed - break; - } - - if (isDelete(op1)) { - operation['delete'](op1); - op1 = ops1[i1++]; - continue; - } - if (isInsert(op2)) { - operation.insert(op2); - op2 = ops2[i2++]; - continue; - } - - if (typeof op1 === 'undefined') { - throw new Error("Cannot compose operations: first operation is too short."); - } - if (typeof op2 === 'undefined') { - throw new Error("Cannot compose operations: first operation is too long."); - } - - if (isRetain(op1) && isRetain(op2)) { - if (op1 > op2) { - operation.retain(op2); - op1 = op1 - op2; - op2 = ops2[i2++]; - } else if (op1 === op2) { - operation.retain(op1); - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - operation.retain(op1); - op2 = op2 - op1; - op1 = ops1[i1++]; - } - } else if (isInsert(op1) && isDelete(op2)) { - if (op1.length > -op2) { - op1 = op1.slice(-op2); - op2 = ops2[i2++]; - } else if (op1.length === -op2) { - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - op2 = op2 + op1.length; - op1 = ops1[i1++]; - } - } else if (isInsert(op1) && isRetain(op2)) { - if (op1.length > op2) { - operation.insert(op1.slice(0, op2)); - op1 = op1.slice(op2); - op2 = ops2[i2++]; - } else if (op1.length === op2) { - operation.insert(op1); - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - operation.insert(op1); - op2 = op2 - op1.length; - op1 = ops1[i1++]; - } - } else if (isRetain(op1) && isDelete(op2)) { - if (op1 > -op2) { - operation['delete'](op2); - op1 = op1 + op2; - op2 = ops2[i2++]; - } else if (op1 === -op2) { - operation['delete'](op2); - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - operation['delete'](op1); - op2 = op2 + op1; - op1 = ops1[i1++]; - } - } else { - throw new Error( - "This shouldn't happen: op1: " + - JSON.stringify(op1) + ", op2: " + - JSON.stringify(op2) - ); - } - } - return operation; - }; - - function getSimpleOp (operation, fn) { - var ops = operation.ops; - var isRetain = TextOperation.isRetain; - switch (ops.length) { - case 1: - return ops[0]; - case 2: - return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null); - case 3: - if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; } - } - return null; - } - - function getStartIndex (operation) { - if (isRetain(operation.ops[0])) { return operation.ops[0]; } - return 0; - } - - // When you use ctrl-z to undo your latest changes, you expect the program not - // to undo every single keystroke but to undo your last sentence you wrote at - // a stretch or the deletion you did by holding the backspace key down. This - // This can be implemented by composing operations on the undo stack. This - // method can help decide whether two operations should be composed. It - // returns true if the operations are consecutive insert operations or both - // operations delete text at the same position. You may want to include other - // factors like the time since the last change in your decision. - TextOperation.prototype.shouldBeComposedWith = function (other) { - if (this.isNoop() || other.isNoop()) { return true; } - - var startA = getStartIndex(this), startB = getStartIndex(other); - var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other); - if (!simpleA || !simpleB) { return false; } - - if (isInsert(simpleA) && isInsert(simpleB)) { - return startA + simpleA.length === startB; - } - - if (isDelete(simpleA) && isDelete(simpleB)) { - // there are two possibilities to delete: with backspace and with the - // delete key. - return (startB - simpleB === startA) || startA === startB; - } - - return false; - }; - - // Decides whether two operations should be composed with each other - // if they were inverted, that is - // `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`. - TextOperation.prototype.shouldBeComposedWithInverted = function (other) { - if (this.isNoop() || other.isNoop()) { return true; } - - var startA = getStartIndex(this), startB = getStartIndex(other); - var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other); - if (!simpleA || !simpleB) { return false; } - - if (isInsert(simpleA) && isInsert(simpleB)) { - return startA + simpleA.length === startB || startA === startB; - } - - if (isDelete(simpleA) && isDelete(simpleB)) { - return startB - simpleB === startA; - } - - return false; - }; - - // Transform takes two operations A and B that happened concurrently and - // produces two operations A' and B' (in an array) such that - // `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the - // heart of OT. - TextOperation.transform = function (operation1, operation2) { - if (operation1.baseLength !== operation2.baseLength) { - throw new Error("Both operations have to have the same base length"); - } - - var operation1prime = new TextOperation(); - var operation2prime = new TextOperation(); - var ops1 = operation1.ops, ops2 = operation2.ops; - var i1 = 0, i2 = 0; - var op1 = ops1[i1++], op2 = ops2[i2++]; - while (true) { - // At every iteration of the loop, the imaginary cursor that both - // operation1 and operation2 have that operates on the input string must - // have the same position in the input string. - - if (typeof op1 === 'undefined' && typeof op2 === 'undefined') { - // end condition: both ops1 and ops2 have been processed - break; - } - - // next two cases: one or both ops are insert ops - // => insert the string in the corresponding prime operation, skip it in - // the other one. If both op1 and op2 are insert ops, prefer op1. - if (isInsert(op1)) { - operation1prime.insert(op1); - operation2prime.retain(op1.length); - op1 = ops1[i1++]; - continue; - } - if (isInsert(op2)) { - operation1prime.retain(op2.length); - operation2prime.insert(op2); - op2 = ops2[i2++]; - continue; - } - - if (typeof op1 === 'undefined') { - throw new Error("Cannot compose operations: first operation is too short."); - } - if (typeof op2 === 'undefined') { - throw new Error("Cannot compose operations: first operation is too long."); - } - - var minl; - if (isRetain(op1) && isRetain(op2)) { - // Simple case: retain/retain - if (op1 > op2) { - minl = op2; - op1 = op1 - op2; - op2 = ops2[i2++]; - } else if (op1 === op2) { - minl = op2; - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - minl = op1; - op2 = op2 - op1; - op1 = ops1[i1++]; - } - operation1prime.retain(minl); - operation2prime.retain(minl); - } else if (isDelete(op1) && isDelete(op2)) { - // Both operations delete the same string at the same position. We don't - // need to produce any operations, we just skip over the delete ops and - // handle the case that one operation deletes more than the other. - if (-op1 > -op2) { - op1 = op1 - op2; - op2 = ops2[i2++]; - } else if (op1 === op2) { - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - op2 = op2 - op1; - op1 = ops1[i1++]; - } - // next two cases: delete/retain and retain/delete - } else if (isDelete(op1) && isRetain(op2)) { - if (-op1 > op2) { - minl = op2; - op1 = op1 + op2; - op2 = ops2[i2++]; - } else if (-op1 === op2) { - minl = op2; - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - minl = -op1; - op2 = op2 + op1; - op1 = ops1[i1++]; - } - operation1prime['delete'](minl); - } else if (isRetain(op1) && isDelete(op2)) { - if (op1 > -op2) { - minl = -op2; - op1 = op1 + op2; - op2 = ops2[i2++]; - } else if (op1 === -op2) { - minl = op1; - op1 = ops1[i1++]; - op2 = ops2[i2++]; - } else { - minl = op1; - op2 = op2 + op1; - op1 = ops1[i1++]; - } - operation2prime['delete'](minl); - } else { - throw new Error("The two operations aren't compatible"); - } - } - - return [operation1prime, operation2prime]; - }; - - return TextOperation; - -}()); - -// Export for CommonJS -if (typeof module === 'object') { - module.exports = ot.TextOperation; -} \ No newline at end of file diff --git a/old_src/lib/ot/wrapped-operation.js b/old_src/lib/ot/wrapped-operation.js deleted file mode 100644 index 91050f4ed..000000000 --- a/old_src/lib/ot/wrapped-operation.js +++ /dev/null @@ -1,80 +0,0 @@ -if (typeof ot === 'undefined') { - // Export for browsers - var ot = {}; -} - -ot.WrappedOperation = (function (global) { - 'use strict'; - - // A WrappedOperation contains an operation and corresponing metadata. - function WrappedOperation (operation, meta) { - this.wrapped = operation; - this.meta = meta; - } - - WrappedOperation.prototype.apply = function () { - return this.wrapped.apply.apply(this.wrapped, arguments); - }; - - WrappedOperation.prototype.invert = function () { - var meta = this.meta; - return new WrappedOperation( - this.wrapped.invert.apply(this.wrapped, arguments), - meta && typeof meta === 'object' && typeof meta.invert === 'function' ? - meta.invert.apply(meta, arguments) : meta - ); - }; - - // Copy all properties from source to target. - function copy (source, target) { - for (var key in source) { - if (source.hasOwnProperty(key)) { - target[key] = source[key]; - } - } - } - - function composeMeta (a, b) { - if (a && typeof a === 'object') { - if (typeof a.compose === 'function') { return a.compose(b); } - var meta = {}; - copy(a, meta); - copy(b, meta); - return meta; - } - return b; - } - - WrappedOperation.prototype.compose = function (other) { - return new WrappedOperation( - this.wrapped.compose(other.wrapped), - composeMeta(this.meta, other.meta) - ); - }; - - function transformMeta (meta, operation) { - if (meta && typeof meta === 'object') { - if (typeof meta.transform === 'function') { - return meta.transform(operation); - } - } - return meta; - } - - WrappedOperation.transform = function (a, b) { - var transform = a.wrapped.constructor.transform; - var pair = transform(a.wrapped, b.wrapped); - return [ - new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), - new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) - ]; - }; - - return WrappedOperation; - -}(this)); - -// Export for CommonJS -if (typeof module === 'object') { - module.exports = ot.WrappedOperation; -} \ No newline at end of file diff --git a/old_src/lib/realtime.ts b/old_src/lib/realtime.ts deleted file mode 100644 index 98d83257f..000000000 --- a/old_src/lib/realtime.ts +++ /dev/null @@ -1,991 +0,0 @@ -import Chance from 'chance' -import CodeMirror from 'codemirror' -import cookie from 'cookie' -import cookieParser from 'cookie-parser' -import moment from 'moment' -import randomcolor from 'randomcolor' -import SocketIO, { Socket } from 'socket.io' -import { config } from './config' - -import { History } from './history' -import { logger } from './logger' -import { Author, Note, Revision, User } from './models' -import { NoteAuthorship } from './models/note' -import { PhotoProfile } from './utils/PhotoProfile' -import { EditorSocketIOServer } from './ot/editor-socketio-server' -import { mapToObject } from './utils/functions' -import { getPermission, Permission } from './web/note/util' - -export type SocketWithNoteId = Socket & { noteId: string } - -const chance = new Chance() - -export enum State { - Starting, - Running, - Stopping -} -/* eslint-disable @typescript-eslint/no-use-before-define */ -const realtime: { - onAuthorizeSuccess: (data, accept) => void; - onAuthorizeFail: (data, message, error, accept) => void; - io: SocketIO.Server; isReady: () => boolean; - connection: (socket: SocketWithNoteId) => void; - secure: (socket: SocketIO.Socket, next: (err?: Error) => void) => void; - getStatus: (callback) => void; state: State; -} = { - io: SocketIO(), - onAuthorizeSuccess: onAuthorizeSuccess, - onAuthorizeFail: onAuthorizeFail, - secure: secure, - connection: connection, - getStatus: getStatus, - isReady: isReady, - state: State.Starting -} -/* eslint-enable @typescript-eslint/no-use-before-define */ - -const disconnectSocketQueue: SocketWithNoteId[] = [] - -function onAuthorizeSuccess (data, accept): void { - accept() -} - -function onAuthorizeFail (data, message, error, accept): void { - accept() // accept whether authorize or not to allow anonymous usage -} - -// secure the origin by the cookie -function secure (socket: Socket, next: (err?: Error) => void): void { - try { - const handshakeData = socket.request - if (handshakeData.headers.cookie) { - handshakeData.cookie = cookie.parse(handshakeData.headers.cookie) - handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionName], config.sessionSecret) - if (handshakeData.sessionID && - handshakeData.cookie[config.sessionName] && - handshakeData.cookie[config.sessionName] !== handshakeData.sessionID) { - logger.debug(`AUTH success cookie: ${handshakeData.sessionID}`) - return next() - } else { - next(new Error('AUTH failed: Cookie is invalid.')) - } - } else { - next(new Error('AUTH failed: No cookie transmitted.')) - } - } catch (ex) { - next(new Error('AUTH failed:' + JSON.stringify(ex))) - } -} - -function emitCheck (note: NoteSession): void { - const out = { - title: note.title, - updatetime: note.updatetime, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: mapToObject(note.authors), - authorship: note.authorship - } - realtime.io.to(note.id).emit('check', out) -} - -class UserSession { - id?: string - address?: string - login?: boolean - userid: string | null - 'user-agent'? - photo: string - color: string - cursor?: CodeMirror.Position - name: string | null - idle?: boolean - type?: string -} - -class NoteSession { - id: string - alias: string - title: string - ownerId: string - ownerprofile: PhotoProfile | null - permission: string - lastchangeuser: string | null - lastchangeuserprofile: PhotoProfile | null - socks: SocketWithNoteId[] - users: Map - tempUsers: Map // time value - createtime: number - updatetime: number - server: EditorSocketIOServer - authors: Map - authorship: NoteAuthorship[] -} - -// actions -const users: Map = new Map() -const notes: Map = new Map() - -let saverSleep = false - -function finishUpdateNote (note: NoteSession, _note: Note, callback: (err: Error | null, note: Note | null) => void): void { - if (!note || !note.server) return callback(null, null) - const body = note.server.document - const title = note.title = Note.parseNoteTitle(body) - const values = { - title: title, - content: body, - authorship: note.authorship, - lastchangeuserId: note.lastchangeuser, - lastchangeAt: Date.now() - } - _note.update(values).then(function (_note) { - saverSleep = false - return callback(null, _note) - }).catch(function (err) { - logger.error(err) - return callback(err, null) - }) -} - -function updateHistory (userId, note: NoteSession, time?): void { - const noteId = note.alias ? note.alias : Note.encodeNoteId(note.id) - if (note.server) History.updateHistory(userId, noteId, note.server.document, time) -} - -function updateNote (note: NoteSession, callback: (err, note) => void): void { - Note.findOne({ - where: { - id: note.id - } - }).then(function (_note) { - if (!_note) return callback(null, null) - // update user note history - const tempUsers = new Map(note.tempUsers) - note.tempUsers = new Map() - for (const [key, time] of tempUsers) { - updateHistory(key, note, time) - } - if (note.lastchangeuser) { - if (_note.lastchangeuserId !== note.lastchangeuser) { - User.findOne({ - where: { - id: note.lastchangeuser - } - }).then(function (user) { - if (!user) return callback(null, null) - note.lastchangeuserprofile = PhotoProfile.fromUser(user) - return finishUpdateNote(note, _note, callback) - }).catch(function (err) { - logger.error(err) - return callback(err, null) - }) - } else { - return finishUpdateNote(note, _note, callback) - } - } else { - note.lastchangeuserprofile = null - return finishUpdateNote(note, _note, callback) - } - }).catch(function (err) { - logger.error(err) - return callback(err, null) - }) -} - -// update when the note is dirty -setInterval(function () { - for (const [key, note] of notes) { - if (note.server.isDirty) { - logger.debug(`updater found dirty note: ${key}`) - note.server.isDirty = false - updateNote(note, function (err, _note) { - // handle when note already been clean up - if (!note || !note.server) return - if (!_note) { - realtime.io.to(note.id).emit('info', { - code: 404 - }) - logger.error('note not found: ', note.id) - } - if (err || !_note) { - for (const sock of note.socks) { - if (sock) { - setTimeout(function () { - sock.disconnect() - }, 0) - } - } - return logger.error('updater error', err) - } - note.updatetime = moment(_note.lastchangeAt).valueOf() - emitCheck(note) - }) - } else { - return - } - } -}, 1000) - -// save note revision in interval -setInterval(function () { - if (saverSleep) return - Revision.saveAllNotesRevision(function (err, notes) { - if (err) return logger.error('revision saver failed: ' + err) - if (notes && notes.length <= 0) { - saverSleep = true - } - }) -}, 60000 * 5) - -let isConnectionBusy: boolean -let isDisconnectBusy: boolean - -const connectionSocketQueue: SocketWithNoteId[] = [] - -function getStatus (callback): void { - Note.count().then(function (notecount) { - const distinctaddresses: string[] = [] - const regaddresses: string[] = [] - const distinctregaddresses: string[] = [] - for (const user of users.values()) { - if (!user) return - let found = false - for (const distinctaddress of distinctaddresses) { - if (user.address === distinctaddress) { - found = true - break - } - } - if (!found) { - if (user.address != null) { - distinctaddresses.push(user.address) - } - } - if (user.login) { - if (user.address != null) { - regaddresses.push(user.address) - } - let found = false - for (let i = 0; i < distinctregaddresses.length; i++) { - if (user.address === distinctregaddresses[i]) { - found = true - break - } - } - if (!found) { - if (user.address != null) { - distinctregaddresses.push(user.address) - } - } - } - } - User.count().then(function (regcount) { - // eslint-disable-next-line standard/no-callback-literal - return callback ? callback({ - onlineNotes: Object.keys(notes).length, - onlineUsers: Object.keys(users).length, - distinctOnlineUsers: distinctaddresses.length, - notesCount: notecount, - registeredUsers: regcount, - onlineRegisteredUsers: regaddresses.length, - distinctOnlineRegisteredUsers: distinctregaddresses.length, - isConnectionBusy: isConnectionBusy, - connectionSocketQueueLength: connectionSocketQueue.length, - isDisconnectBusy: isDisconnectBusy, - disconnectSocketQueueLength: disconnectSocketQueue.length - }) : null - }).catch(function (err) { - return logger.error('count user failed: ' + err) - }) - }).catch(function (err) { - return logger.error('count note failed: ' + err) - }) -} - -function isReady (): boolean { - return realtime.io && - Object.keys(notes).length === 0 && Object.keys(users).length === 0 && - connectionSocketQueue.length === 0 && !isConnectionBusy && - disconnectSocketQueue.length === 0 && !isDisconnectBusy -} - -function extractNoteIdFromSocket (socket: Socket): string | boolean { - if (!socket || !socket.handshake) { - return false - } - if (socket.handshake.query && socket.handshake.query.noteId) { - return socket.handshake.query.noteId - } else { - return false - } -} - -function parseNoteIdFromSocket (socket: Socket, callback: (err: string | null, noteId: string | null) => void): void { - const noteId = extractNoteIdFromSocket(socket) - if (!noteId) { - return callback(null, null) - } - Note.parseNoteId(noteId, function (err, id) { - if (err || !id) return callback(err, id) - return callback(null, id) - }) -} - -function buildUserOutData (user): UserSession { - return { - id: user.id, - login: user.login, - userid: user.userid, - photo: user.photo, - color: user.color, - cursor: user.cursor, - name: user.name, - idle: user.idle, - type: user.type - } -} - -function emitOnlineUsers (socket: SocketWithNoteId): void { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const users: UserSession[] = [] - for (const user of note.users.values()) { - if (user) { - users.push(buildUserOutData(user)) - } - } - const out = { - users: users - } - realtime.io.to(noteId).emit('online users', out) -} - -function emitUserStatus (socket: SocketWithNoteId): void { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const user = users.get(socket.id) - if (!user) return - const out = buildUserOutData(user) - socket.broadcast.to(noteId).emit('user status', out) -} - -function emitRefresh (socket: SocketWithNoteId): void { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const out = { - title: note.title, - docmaxlength: config.documentMaxLength, - owner: note.ownerId, - ownerprofile: note.ownerprofile, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: mapToObject(note.authors), - authorship: note.authorship, - permission: note.permission, - createtime: note.createtime, - updatetime: note.updatetime - } - socket.emit('refresh', out) -} - -function isDuplicatedInSocketQueue (queue: Socket[], socket: Socket): boolean { - for (const sock of queue) { - if (sock && sock.id === socket.id) { - return true - } - } - return false -} - -function clearSocketQueue (queue: Socket[], socket: Socket): void { - for (let i = 0; i < queue.length; i++) { - if (!queue[i] || queue[i].id === socket.id) { - queue.splice(i, 1) - i-- - } - } -} - -function connectNextSocket (): void { - setTimeout(function () { - isConnectionBusy = false - if (connectionSocketQueue.length > 0) { - // Otherwise we get a loop startConnection - failConnection - connectNextSocket - // eslint-disable-next-line @typescript-eslint/no-use-before-define - startConnection(connectionSocketQueue[0]) - } - }, 1) -} - -function failConnection (errorCode: number, errorMessage: string, socket: Socket): void { - logger.error(errorMessage) - // clear error socket in queue - clearSocketQueue(connectionSocketQueue, socket) - connectNextSocket() - // emit error info - socket.emit('info', { - code: errorCode - }) - socket.disconnect(true) -} - -function interruptConnection (socket: Socket, noteId: string, socketId): void { - notes.delete(noteId) - users.delete(socketId) - if (socket) { - clearSocketQueue(connectionSocketQueue, socket) - } else { - connectionSocketQueue.shift() - } - connectNextSocket() -} - -function finishConnection (socket: SocketWithNoteId, noteId: string, socketId: string): void { - // if no valid info provided will drop the client - if (!socket || !notes.get(noteId) || !users.get(socketId)) { - return interruptConnection(socket, noteId, socketId) - } - // check view permission - const note = notes.get(noteId) - if (!note) return - if (getPermission(socket.request.user, note) === Permission.None) { - interruptConnection(socket, noteId, socketId) - return failConnection(403, 'connection forbidden', socket) - } - const user = users.get(socketId) - if (!user) { - logger.warn('Could not find user for socketId ' + socketId) - return - } - if (user.userid) { - // update user color to author color - const author = note.authors.get(user.userid) - if (author) { - const socketIdUser = users.get(socket.id) - if (!socketIdUser) return - user.color = author.color - socketIdUser.color = author.color - users.set(socket.id, user) - } - } - note.users.set(socket.id, user) - note.socks.push(socket) - note.server.addClient(socket) - note.server.setName(socket, user.name) - note.server.setColor(socket, user.color) - - // update user note history - updateHistory(user.userid, note) - - emitOnlineUsers(socket) - emitRefresh(socket) - - // clear finished socket in queue - clearSocketQueue(connectionSocketQueue, socket) - // seek for next socket - connectNextSocket() - - if (config.debug) { - const noteId = socket.noteId - logger.debug(`SERVER connected a client to [${noteId}]:`) - logger.debug(JSON.stringify(user)) - logger.debug(notes) - getStatus(function (data) { - logger.debug(JSON.stringify(data)) - }) - } -} - -function ifMayEdit (socket: SocketWithNoteId, originIsOperation: boolean, callback: (mayEdit: boolean) => void): void { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const mayEdit = (getPermission(socket.request.user, note) >= Permission.Write) - // if user may edit and this is a text operation - if (originIsOperation && mayEdit) { - // save for the last change user id - if (socket.request.user && socket.request.user.logged_in) { - note.lastchangeuser = socket.request.user.id - } else { - note.lastchangeuser = null - } - } - return callback(mayEdit) -} - -function operationCallback (socket: SocketWithNoteId, operation): void { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - let userId: string | null = null - // save authors - if (socket.request.user && socket.request.user.logged_in) { - const user = users.get(socket.id) - if (!user) return - userId = socket.request.user.id - if (!userId) return - const author = note.authors.get(userId) - if (!author) { - Author.findOrCreate({ - where: { - noteId: noteId, - userId: userId - }, - defaults: { - noteId: noteId, - userId: userId, - color: user.color - } - }).then(function ([author, _]) { - if (author) { - note.authors.set(author.userId, { - userid: author.userId, - color: author.color, - photo: user.photo, - name: user.name - }) - } - }).catch(function (err) { - logger.error('operation callback failed: ' + err) - }) - } - if (userId) note.tempUsers.set(userId, Date.now()) - } - // save authorship - use timer here because it's an O(n) complexity algorithm - setImmediate(function () { - note.authorship = Note.updateAuthorshipByOperation(operation, userId, note.authorship) - }) -} - -function startConnection (socket: SocketWithNoteId): void { - if (isConnectionBusy) return - isConnectionBusy = true - - const noteId: string = socket.noteId - if (!noteId) { - return failConnection(404, 'note id not found', socket) - } - - if (!notes.get(noteId)) { - const include = [{ - model: User, - as: 'owner' - }, { - model: User, - as: 'lastchangeuser' - }, { - model: Author, - as: 'authors', - include: [{ - model: User, - as: 'user' - }] - }] - - Note.findOne({ - where: { - id: noteId - }, - include: include - }).then(function (note) { - if (!note) { - return failConnection(404, 'note not found', socket) - } - const ownerId = note.ownerId - const ownerprofile = note.owner ? PhotoProfile.fromUser(note.owner) : null - - const lastchangeuser = note.lastchangeuserId - const lastchangeuserprofile = note.lastchangeuser ? PhotoProfile.fromUser(note.lastchangeuser) : null - - const body = note.content - const createtime = note.createdAt - const updatetime = note.lastchangeAt - const server = new EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback) - - const authors = new Map() - for (const author of note.authors) { - const profile = PhotoProfile.fromUser(author.user) - if (profile) { - authors.set(author.userId, { - userid: author.userId, - color: author.color, - photo: profile.photo, - name: profile.name - }) - } - } - - notes.set(noteId, { - id: noteId, - alias: note.alias, - title: note.title, - ownerId: ownerId, - ownerprofile: ownerprofile, - permission: note.permission, - lastchangeuser: lastchangeuser, - lastchangeuserprofile: lastchangeuserprofile, - socks: [], - users: new Map(), - tempUsers: new Map(), - createtime: moment(createtime).valueOf(), - updatetime: moment(updatetime).valueOf(), - server: server, - authors: authors, - authorship: note.authorship - }) - - return finishConnection(socket, noteId, socket.id) - }).catch(function (err) { - return failConnection(500, err, socket) - }) - } else { - return finishConnection(socket, noteId, socket.id) - } -} - -isConnectionBusy = false -isDisconnectBusy = false - -function disconnect (socket: SocketWithNoteId): void { - if (isDisconnectBusy) return - isDisconnectBusy = true - - logger.debug('SERVER disconnected a client') - logger.debug(JSON.stringify(users.get(socket.id))) - - if (users.get(socket.id)) { - users.delete(socket.id) - } - const noteId = socket.noteId - const note = notes.get(noteId) - if (note) { - // delete user in users - if (note.users.get(socket.id)) { - note.users.delete(socket.id) - } - // remove sockets in the note socks - let index - do { - index = note.socks.indexOf(socket) - if (index !== -1) { - note.socks.splice(index, 1) - } - } while (index !== -1) - // remove note in notes if no user inside - if (note.users.size <= 0) { - if (note.server.isDirty) { - updateNote(note, function (err, _) { - if (err) return logger.error('disconnect note failed: ' + err) - // clear server before delete to avoid memory leaks - note.server.document = '' - note.server.operations = [] - delete note.server - notes.delete(noteId) - if (config.debug) { - logger.debug(notes) - getStatus(function (data) { - logger.debug(JSON.stringify(data)) - }) - } - }) - } else { - delete note.server - notes.delete(noteId) - } - } - } - emitOnlineUsers(socket) - - // clear finished socket in queue - clearSocketQueue(disconnectSocketQueue, socket) - // seek for next socket - isDisconnectBusy = false - if (disconnectSocketQueue.length > 0) { - disconnect(disconnectSocketQueue[0]) - } - - if (config.debug) { - logger.debug(notes) - getStatus(function (data) { - logger.debug(JSON.stringify(data)) - }) - } -} - -// clean when user not in any rooms or user not in connected list -setInterval(function () { - for (const [key, user] of users) { - let socket = realtime.io.sockets.connected[key] as SocketWithNoteId - if ((!socket && user) || - (socket && (!socket.rooms || Object.keys(socket.rooms).length <= 0))) { - logger.debug(`cleaner found redundant user: ${key}`) - if (!socket) { - socket = { - id: key - } as SocketWithNoteId - } - disconnectSocketQueue.push(socket) - disconnect(socket) - } - } -}, 60000) - -function updateUserData (socket: Socket, user): void { - // retrieve user data from passport - if (socket.request.user && socket.request.user.logged_in) { - const profile = PhotoProfile.fromUser(socket.request.user) - user.photo = profile?.photo - user.name = profile?.name - user.userid = socket.request.user.id - user.login = true - } else { - user.userid = null - user.name = 'Guest ' + chance.last() - user.login = false - } -} - -function connection (socket: SocketWithNoteId): void { - if (realtime.state !== State.Running) return - parseNoteIdFromSocket(socket, function (err, noteId) { - if (err) { - return failConnection(500, err, socket) - } - if (!noteId) { - return failConnection(404, 'note id not found', socket) - } - - if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return - - // store noteId in this socket session - socket.noteId = noteId - - // initialize user data - // random color - let color = randomcolor() - // make sure color not duplicated or reach max random count - const note = notes.get(noteId) - if (note) { - let randomcount = 0 - const maxrandomcount = 10 - let found = false - do { - for (const user of note.users.values()) { - if (user.color === color) { - found = true - } - } - if (found) { - color = randomcolor() - randomcount++ - } - } while (found && randomcount < maxrandomcount) - } - // create user data - users.set(socket.id, { - id: socket.id, - address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, - 'user-agent': socket.handshake.headers['user-agent'], - color: color, - cursor: undefined, - login: false, - userid: null, - name: null, - idle: false, - type: '', - photo: '' - }) - updateUserData(socket, users.get(socket.id)) - - // start connection - connectionSocketQueue.push(socket) - startConnection(socket) - }) - - // received client refresh request - socket.on('refresh', function () { - emitRefresh(socket) - }) - - // received user status - socket.on('user status', function (data) { - const noteId = socket.noteId - const user = users.get(socket.id) - if (!noteId || !notes.get(noteId) || !user) return - logger.debug(`SERVER received [${noteId}] user status from [${socket.id}]: ${JSON.stringify(data)}`) - if (data) { - user.idle = data.idle - user.type = data.type - } - emitUserStatus(socket) - }) - - // received note permission change request - socket.on('permission', function (permission) { - // need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - // Only owner can change permission - if (getPermission(socket.request.user, note) === Permission.Owner) { - if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return - note.permission = permission - Note.update({ - permission: permission - }, { - where: { - id: noteId - } - }).then(function (count) { - if (!count) { - return - } - const out = { - permission: permission - } - realtime.io.to(note.id).emit('permission', out) - for (let i = 0, l = note.socks.length; i < l; i++) { - const sock = note.socks[i] - if (typeof sock !== 'undefined' && sock) { - // check view permission - if (getPermission(sock.request.user, note) === Permission.None) { - sock.emit('info', { - code: 403 - }) - setTimeout(function () { - sock.disconnect(true) - }, 0) - } - } - } - }).catch(function (err) { - return logger.error('update note permission failed: ' + err) - }) - } - } - }) - - // delete a note - socket.on('delete', function () { - // need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - // Only owner can delete note - if (getPermission(socket.request.user, note) === Permission.Owner) { - Note.destroy({ - where: { - id: noteId - } - }).then(function (count) { - if (!count) return - for (let i = 0, l = note.socks.length; i < l; i++) { - const sock = note.socks[i] - if (typeof sock !== 'undefined' && sock) { - sock.emit('delete') - setTimeout(function () { - sock.disconnect(true) - }, 0) - } - } - }).catch(function (err) { - return logger.error('delete note failed: ' + err) - }) - } - } - }) - - // reveiced when user logout or changed - socket.on('user changed', function () { - logger.info('user changed') - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const user = note.users.get(socket.id) - if (!user) return - updateUserData(socket, user) - emitOnlineUsers(socket) - }) - - // received sync of online users request - socket.on('online users', function () { - const noteId = socket.noteId - if (!noteId) return - const note = notes.get(noteId) - if (!note) return - const users: UserSession[] = [] - for (const user of note.users.values()) { - if (user) { - users.push(buildUserOutData(user)) - } - } - const out = { - users: users - } - socket.emit('online users', out) - }) - - // check version - socket.on('version', function () { - socket.emit('version', { - version: config.fullversion, - minimumCompatibleVersion: config.minimumCompatibleVersion - }) - }) - - // received cursor focus - socket.on('cursor focus', function (data) { - const noteId = socket.noteId - const user = users.get(socket.id) - if (!noteId || !notes.get(noteId) || !user) return - user.cursor = data - const out = buildUserOutData(user) - socket.broadcast.to(noteId).emit('cursor focus', out) - }) - - // received cursor activity - socket.on('cursor activity', function (data: CodeMirror.Position) { - const noteId = socket.noteId - const user = users.get(socket.id) - if (!noteId || !notes.get(noteId) || !user) return - user.cursor = data - const out = buildUserOutData(user) - socket.broadcast.to(noteId).emit('cursor activity', out) - }) - - // received cursor blur - socket.on('cursor blur', function () { - const noteId = socket.noteId - const user = users.get(socket.id) - if (!noteId || !notes.get(noteId) || !user) return - user.cursor = undefined - const out = { - id: socket.id - } - socket.broadcast.to(noteId).emit('cursor blur', out) - }) - - // when a new client disconnect - socket.on('disconnect', function () { - if (isDuplicatedInSocketQueue(disconnectSocketQueue, socket)) return - disconnectSocketQueue.push(socket) - disconnect(socket) - }) -} - -export { realtime } diff --git a/old_src/lib/response.ts b/old_src/lib/response.ts deleted file mode 100644 index f01d576ec..000000000 --- a/old_src/lib/response.ts +++ /dev/null @@ -1,184 +0,0 @@ -'use strict' -import { config } from './config' -import { Note, User } from './models' - -import fs from 'fs' - -import { logger } from './logger' - -import * as NoteUtils from './web/note/util' - -import { errors } from './errors' - -import path from 'path' - -import request from 'request' - -function showIndex (req, res, _): void { - const authStatus = req.isAuthenticated() - const deleteToken = '' - - const data = { - signin: authStatus, - infoMessage: req.flash('info'), - errorMessage: req.flash('error'), - imprint: fs.existsSync(path.join(config.docsPath, 'imprint.md')), - privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), - termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')), - deleteToken: deleteToken - } - - if (authStatus) { - User.findOne({ - where: { - id: req.user.id - } - }).then(function (user: User | null) { - if (user) { - data.deleteToken = user.deleteToken - res.render('index.ejs', data) - } - }) - } else { - res.render('index.ejs', data) - } -} - -function githubActionGist (req, res, note: Note): void { - const code = req.query.code - const state = req.query.state - if (!code || !state) { - return errors.errorForbidden(res) - } else { - // This is the way the github api works, therefore we can't change it to camelcase - const data = { - // eslint-disable-next-line @typescript-eslint/camelcase - client_id: config.github.clientID, - // eslint-disable-next-line @typescript-eslint/camelcase - client_secret: config.github.clientSecret, - code: code, - state: state - } - const authUrl = 'https://github.com/login/oauth/access_token' - request({ - url: authUrl, - method: 'POST', - json: data - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode === 200) { - const accessToken = body.access_token - if (accessToken) { - const content = note.content - const title = Note.decodeTitle(note.title) - const filename = title.replace('/', ' ') + '.md' - const gist = { - files: {} - } - gist.files[filename] = { - content: content - } - const gistUrl = 'https://api.github.com/gists' - request({ - url: gistUrl, - headers: { - 'User-Agent': 'CodiMD', - Authorization: 'token ' + accessToken - }, - method: 'POST', - json: gist - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode === 201) { - res.setHeader('referer', '') - res.redirect(body.html_url) - } else { - errors.errorForbidden(res) - } - }) - } else { - errors.errorForbidden(res) - } - } else { - errors.errorForbidden(res) - } - }) - } -} - -function githubActions (req, res, _): void { - const noteId = req.params.noteId - NoteUtils.findNoteOrCreate(req, res, function (note: Note) { - const action = req.params.action - switch (action) { - case 'gist': - githubActionGist(req, res, note) - break - default: - res.redirect(config.serverURL + '/' + noteId) - break - } - }) -} - -function gitlabActionProjects (req, res, _): void { - if (req.isAuthenticated()) { - User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) { - errors.errorNotFound(res) - return - } - class GitlabReturn { - baseURL; - version; - accesstoken; - profileid; - projects; - } - const ret: GitlabReturn = new GitlabReturn() - ret.baseURL = config.gitlab.baseURL - ret.version = config.gitlab.version - ret.accesstoken = user.accessToken - ret.profileid = user.profileid - request( - config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken, - function (error, httpResponse, body) { - if (!error && httpResponse.statusCode === 200) { - ret.projects = JSON.parse(body) - return res.send(ret) - } else { - return res.send(ret) - } - } - ) - }).catch(function (err) { - logger.error('gitlab action projects failed: ' + err) - errors.errorInternalError(res) - }) - } else { - errors.errorForbidden(res) - } -} - -function gitlabActions (req, res, _): void { - const noteId = req.params.noteId - NoteUtils.findNoteOrCreate(req, res, function (note) { - const action = req.params.action - switch (action) { - case 'projects': - gitlabActionProjects(req, res, note) - break - default: - res.redirect(config.serverURL + '/' + noteId) - break - } - }) -} - -export const response = { - showIndex: showIndex, - githubActions: githubActions, - gitlabActions: gitlabActions -} diff --git a/old_src/lib/utils/PhotoProfile.ts b/old_src/lib/utils/PhotoProfile.ts deleted file mode 100644 index 85c534dc8..000000000 --- a/old_src/lib/utils/PhotoProfile.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { generateAvatarURL } from '../letter-avatars' -import { logger } from '../logger' -import { PassportProfile, ProviderEnum } from '../web/auth/utils' -import { User } from '../models' - -export class PhotoProfile { - name: string - photo: string - biggerphoto: string - - static fromUser (user: User): PhotoProfile | null { - if (!user) return null - if (user.profile) return PhotoProfile.fromJSON(user.profile) - if (user.email) return PhotoProfile.fromEmail(user.email) - return null - } - - private static fromJSON (jsonProfile: string): PhotoProfile | null { - try { - const parsedProfile: PassportProfile = JSON.parse(jsonProfile) - return { - name: parsedProfile.displayName || parsedProfile.username, - photo: PhotoProfile.generatePhotoURL(parsedProfile, false), - biggerphoto: PhotoProfile.generatePhotoURL(parsedProfile, true) - } - } catch (err) { - logger.error(err) - return null - } - } - - private static fromEmail (email: string): PhotoProfile { - return { - name: email.substring(0, email.lastIndexOf('@')), - photo: generateAvatarURL('', email, false), - biggerphoto: generateAvatarURL('', email, true) - } - } - - private static generatePhotoURL (profile: PassportProfile, bigger: boolean): string { - let photo: string - switch (profile.provider) { - case ProviderEnum.facebook: - photo = 'https://graph.facebook.com/' + profile.id + '/picture' - if (bigger) { - photo += '?width=400' - } else { - photo += '?width=96' - } - break - case ProviderEnum.twitter: - photo = 'https://twitter.com/' + profile.username + '/profile_image' - if (bigger) { - photo += '?size=original' - } else { - photo += '?size=bigger' - } - break - case ProviderEnum.github: - photo = 'https://avatars.githubusercontent.com/u/' + profile.id - if (bigger) { - photo += '?s=400' - } else { - photo += '?s=96' - } - break - case ProviderEnum.gitlab: - photo = profile.avatarUrl - if (photo) { - if (bigger) { - photo = photo.replace(/(\?s=)\d*$/i, '$1400') - } else { - photo = photo.replace(/(\?s=)\d*$/i, '$196') - } - } else { - photo = generateAvatarURL(profile.username) - } - break - case ProviderEnum.dropbox: - photo = generateAvatarURL('', profile.emails[0], bigger) - break - case ProviderEnum.google: - photo = profile.photos[0].value - if (bigger) { - photo = photo.replace(/(\?sz=)\d*$/i, '$1400') - } else { - photo = photo.replace(/(\?sz=)\d*$/i, '$196') - } - break - case ProviderEnum.ldap: - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - case ProviderEnum.saml: - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - default: - photo = generateAvatarURL(profile.username) - break - } - return photo - } -} diff --git a/old_src/lib/utils/functions.ts b/old_src/lib/utils/functions.ts deleted file mode 100644 index 1ddac94e3..000000000 --- a/old_src/lib/utils/functions.ts +++ /dev/null @@ -1,96 +0,0 @@ -import fs from 'fs' -import { config } from '../config' -import { logger } from '../logger' -import { Revision } from '../models' -import { realtime, State } from '../realtime' - -/* -Converts a map from string to something into a plain JS object for transmitting via a websocket - */ -export function mapToObject (map: Map): object { - return Array.from(map).reduce((obj, [key, value]) => { - obj[key] = value - return obj - }, {}) -} - -export function getImageMimeType (imagePath: string): string | undefined { - const fileExtension = /[^.]+$/.exec(imagePath) - switch (fileExtension?.[0]) { - case 'bmp': - return 'image/bmp' - case 'gif': - return 'image/gif' - case 'jpg': - case 'jpeg': - return 'image/jpeg' - case 'png': - return 'image/png' - case 'tiff': - return 'image/tiff' - case 'svg': - return 'image/svg+xml' - default: - return undefined - } -} - -// [Postgres] Handling NULL bytes -// https://github.com/sequelize/sequelize/issues/6485 -export function stripNullByte (value: string): string { - value = '' + value - // eslint-disable-next-line no-control-regex - return value ? value.replace(/\u0000/g, '') : value -} - -export function processData (data: T, _default: T, process?: (T) => T): T | undefined { - if (data === undefined) return undefined - else if (data === null) return _default - else if (process) return process(data) - else return data -} - -export function handleTermSignals (io): void { - if (realtime.state === State.Starting) { - process.exit(0) - } - if (realtime.state === State.Stopping) { - // The function is already running. Do nothing - return - } - logger.info('CodiMD has been killed by signal, try to exit gracefully...') - realtime.state = State.Stopping - // disconnect all socket.io clients - Object.keys(io.sockets.sockets).forEach(function (key) { - const socket = io.sockets.sockets[key] - // notify client server going into maintenance status - socket.emit('maintenance') - setTimeout(function () { - socket.disconnect(true) - }, 0) - }) - if (config.path) { - // ToDo: add a proper error handler - // eslint-disable-next-line @typescript-eslint/no-empty-function - fs.unlink(config.path, (_) => { - }) - } - const checkCleanTimer = setInterval(function () { - if (realtime.isReady()) { - Revision.checkAllNotesRevision(function (err, notes) { - if (err) { - return logger.error('Error while writing changes to database. We will abort after trying for 30 seconds.\n' + err) - } - if (!notes || notes.length <= 0) { - clearInterval(checkCleanTimer) - return process.exit(0) - } - }) - } - }, 500) - setTimeout(function () { - logger.error('Failed to write changes to database. Aborting') - clearInterval(checkCleanTimer) - process.exit(1) - }, 30000) -} diff --git a/old_src/lib/web/auth/dropbox/index.ts b/old_src/lib/web/auth/dropbox/index.ts deleted file mode 100644 index a6f4d750e..000000000 --- a/old_src/lib/web/auth/dropbox/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextFunction, Request, Response, Router } from 'express' -import passport from 'passport' -import { Strategy as DropboxStrategy } from 'passport-dropbox-oauth2' -import { config } from '../../../config' -import { User } from '../../../models' -import { AuthMiddleware } from '../interface' -import { passportGeneralCallback } from '../utils' - -export const dropboxAuth = Router() - -export const DropboxMiddleware: AuthMiddleware = { - getMiddleware (): Router { - passport.use(new DropboxStrategy({ - apiVersion: '2', - clientID: config.dropbox.clientID, - clientSecret: config.dropbox.clientSecret, - callbackURL: config.serverURL + '/auth/dropbox/callback' - }, ( - accessToken: string, - refreshToken: string, - profile, - done: (err?: Error | null, user?: User) => void - ): void => { - // the Dropbox plugin wraps the email addresses in an object - // see https://github.com/florianheinemann/passport-dropbox-oauth2/blob/master/lib/passport-dropbox-oauth2/strategy.js#L146 - profile.emails = profile.emails.map(element => element.value) - passportGeneralCallback(accessToken, refreshToken, profile, done) - })) - - dropboxAuth.get('/auth/dropbox', function (req: Request, res: Response, next: NextFunction) { - passport.authenticate('dropbox-oauth2')(req, res, next) - }) - dropboxAuth.get('/auth/dropbox/callback', - passport.authenticate('dropbox-oauth2', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - return dropboxAuth - } -} diff --git a/old_src/lib/web/auth/email/index.ts b/old_src/lib/web/auth/email/index.ts deleted file mode 100644 index e153c9fc4..000000000 --- a/old_src/lib/web/auth/email/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { NextFunction, Request, Response, Router } from 'express' -import passport from 'passport' -import { Strategy as LocalStrategy } from 'passport-local' -import validator from 'validator' -import { config } from '../../../config' -import { errors } from '../../../errors' -import { logger } from '../../../logger' -import { User } from '../../../models' -import { urlencodedParser } from '../../utils' -import { AuthMiddleware } from '../interface' - -const emailAuth = Router() - -export const EmailMiddleware: AuthMiddleware = { - getMiddleware (): Router { - passport.use(new LocalStrategy({ - usernameField: 'email' - }, function (email: string, password: string, done) { - if (!validator.isEmail(email)) return done(null, false) - User.findOne({ - where: { - email: email - } - }).then(function (user: User) { - if (!user) return done(null, false) - user.verifyPassword(password).then(verified => { - if (verified) { - return done(null, user) - } else { - logger.warn('invalid password given for %s', user.email) - return done(null, false) - } - }) - }).catch(function (err: Error) { - logger.error(err) - return done(err) - }) - })) - - if (config.allowEmailRegister) { - emailAuth.post('/register', urlencodedParser, function (req: Request, res: Response, _: NextFunction) { - if (!req.body.email || !req.body.password) { - errors.errorBadRequest(res) - return - } - if (!validator.isEmail(req.body.email)) { - errors.errorBadRequest(res) - return - } - User.findOrCreate({ - where: { - email: req.body.email - }, - defaults: { - password: req.body.password - } - }).then(function ([user, created]: [User, boolean]) { - if (user) { - if (created) { - logger.debug('user registered: ' + user.id) - req.flash('info', "You've successfully registered, please signin.") - return res.redirect(config.serverURL + '/') - } else { - logger.debug('user found: ' + user.id) - req.flash('error', 'This email has been used, please try another one.') - return res.redirect(config.serverURL + '/') - } - } - req.flash('error', 'Failed to register your account, please try again.') - return res.redirect(config.serverURL + '/') - }).catch(function (err) { - logger.error('auth callback failed: ' + err) - errors.errorInternalError(res) - }) - }) - } - - emailAuth.post('/login', urlencodedParser, function (req: Request, res: Response, next: NextFunction) { - if (!req.body.email || !req.body.password) { - errors.errorBadRequest(res) - return - } - if (!validator.isEmail(req.body.email)) { - errors.errorBadRequest(res) - return - } - passport.authenticate('local', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/', - failureFlash: 'Invalid email or password.' - })(req, res, next) - }) - return emailAuth - } -} diff --git a/old_src/lib/web/auth/facebook/index.ts b/old_src/lib/web/auth/facebook/index.ts deleted file mode 100644 index a516a32e8..000000000 --- a/old_src/lib/web/auth/facebook/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import passport from 'passport' -import { config } from '../../../config' -import { AuthMiddleware } from '../interface' -import { Router } from 'express' -import { passportGeneralCallback } from '../utils' -import { Strategy as FacebookStrategy } from 'passport-facebook' - -export const FacebookMiddleware: AuthMiddleware = { - getMiddleware (): Router { - const facebookAuth = Router() - passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.serverURL + '/auth/facebook/callback' - }, passportGeneralCallback - )) - - facebookAuth.get('/auth/facebook', function (req, res, next) { - passport.authenticate('facebook')(req, res, next) - }) - - // facebook auth callback - facebookAuth.get('/auth/facebook/callback', - passport.authenticate('facebook', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - return facebookAuth - } -} diff --git a/old_src/lib/web/auth/github/index.ts b/old_src/lib/web/auth/github/index.ts deleted file mode 100644 index 15d2b67aa..000000000 --- a/old_src/lib/web/auth/github/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import { Strategy as GithubStrategy } from 'passport-github' -import { config } from '../../../config' -import { response } from '../../../response' -import { AuthMiddleware } from '../interface' -import { passportGeneralCallback } from '../utils' - -export const GithubMiddleware: AuthMiddleware = { - getMiddleware (): Router { - const githubAuth = Router() - - passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.serverURL + '/auth/github/callback' - }, passportGeneralCallback)) - - githubAuth.get('/auth/github', function (req, res, next) { - passport.authenticate('github')(req, res, next) - }) - - // github auth callback - githubAuth.get('/auth/github/callback', - passport.authenticate('github', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - // github callback actions - githubAuth.get('/auth/github/callback/:noteId/:action', response.githubActions) - - return githubAuth - } -} diff --git a/old_src/lib/web/auth/gitlab/index.ts b/old_src/lib/web/auth/gitlab/index.ts deleted file mode 100644 index adeec8ebe..000000000 --- a/old_src/lib/web/auth/gitlab/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import { Strategy as GitlabStrategy } from 'passport-gitlab2' -import { config } from '../../../config' -import { response } from '../../../response' -import { AuthMiddleware } from '../interface' -import { passportGeneralCallback } from '../utils' - -export const GitlabMiddleware: AuthMiddleware = - { - getMiddleware (): Router { - const gitlabAuth = module.exports = Router() - - passport.use(new GitlabStrategy({ - baseURL: config.gitlab.baseURL, - clientID: config.gitlab.clientID, - clientSecret: config.gitlab.clientSecret, - scope: config.gitlab.scope, - callbackURL: config.serverURL + '/auth/gitlab/callback' - }, passportGeneralCallback)) - - gitlabAuth.get('/auth/gitlab', function (req, res, next) { - passport.authenticate('gitlab')(req, res, next) - }) - - // gitlab auth callback - gitlabAuth.get('/auth/gitlab/callback', - passport.authenticate('gitlab', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - if (!config.gitlab.scope || config.gitlab.scope === 'api' - ) { - // gitlab callback actions - gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) - } - return gitlabAuth - } - } diff --git a/old_src/lib/web/auth/google/index.ts b/old_src/lib/web/auth/google/index.ts deleted file mode 100644 index 165d1f7e5..000000000 --- a/old_src/lib/web/auth/google/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import * as Google from 'passport-google-oauth20' -import { config } from '../../../config' -import { AuthMiddleware } from '../interface' -import { passportGeneralCallback } from '../utils' - -const googleAuth = Router() - -export const GoogleMiddleware: AuthMiddleware = { - getMiddleware: function (): Router { - passport.use(new Google.Strategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.serverURL + '/auth/google/callback', - userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' - }, ( - accessToken: string, - refreshToken: string, - profile, - done) => { - /* - This ugly hack is neccessary, because the Google Strategy wants a done-callback with an err as Error | null | undefined - but the passportGeneralCallback (and every other PassportStrategy) want a done-callback with err as string | Error | undefined - Note the absence of null. The lambda converts all `null` to `undefined`. - */ - passportGeneralCallback(accessToken, refreshToken, profile, (err?, user?) => { - done(err === null ? undefined : err, user) - }) - })) - - googleAuth.get('/auth/google', function (req, res, next) { - const authOpts = { scope: ['profile'], hostedDomain: config.google.hostedDomain } - passport.authenticate('google', authOpts)(req, res, next) - }) - googleAuth.get('/auth/google/callback', - passport.authenticate('google', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - return googleAuth - } -} diff --git a/old_src/lib/web/auth/index.ts b/old_src/lib/web/auth/index.ts deleted file mode 100644 index f79e99eda..000000000 --- a/old_src/lib/web/auth/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Request, Response, Router } from 'express' -import passport from 'passport' -import { config } from '../../config' -import { logger } from '../../logger' -import { User } from '../../models' -import { FacebookMiddleware } from './facebook' -import { TwitterMiddleware } from './twitter' -import { GithubMiddleware } from './github' -import { GitlabMiddleware } from './gitlab' -import { DropboxMiddleware } from './dropbox' -import { GoogleMiddleware } from './google' -import { LdapMiddleware } from './ldap' -import { SamlMiddleware } from './saml' -import { OAuth2Middleware } from './oauth2' -import { EmailMiddleware } from './email' -import { OPenIDMiddleware } from './openid' - -const AuthRouter = Router() - -// serialize and deserialize -passport.serializeUser(function (user: User, done) { - logger.info('serializeUser: ' + user.id) - return done(null, user.id) -}) - -passport.deserializeUser(function (id: string, done) { - User.findOne({ - where: { - id: id - } - }).then(function (user) { - // Don't die on non-existent user - if (user == null) { - // The extra object with message doesn't exits in @types/passport - return done(null, false) // , { message: 'Invalid UserID' }) - } - - logger.info('deserializeUser: ' + user.id) - return done(null, user) - }).catch(function (err) { - logger.error(err) - return done(err, null) - }) -}) - -if (config.isFacebookEnable) AuthRouter.use(FacebookMiddleware.getMiddleware()) -if (config.isTwitterEnable) AuthRouter.use(TwitterMiddleware.getMiddleware()) -if (config.isGitHubEnable) AuthRouter.use(GithubMiddleware.getMiddleware()) -if (config.isGitLabEnable) AuthRouter.use(GitlabMiddleware.getMiddleware()) -if (config.isDropboxEnable) AuthRouter.use(DropboxMiddleware.getMiddleware()) -if (config.isGoogleEnable) AuthRouter.use(GoogleMiddleware.getMiddleware()) -if (config.isLDAPEnable) AuthRouter.use(LdapMiddleware.getMiddleware()) -if (config.isSAMLEnable) AuthRouter.use(SamlMiddleware.getMiddleware()) -if (config.isOAuth2Enable) AuthRouter.use(OAuth2Middleware.getMiddleware()) -if (config.isEmailEnable) AuthRouter.use(EmailMiddleware.getMiddleware()) -if (config.isOpenIDEnable) AuthRouter.use(OPenIDMiddleware.getMiddleware()) - -// logout -AuthRouter.get('/logout', function (req: Request, res: Response) { - if (config.debug && req.isAuthenticated()) { - if (req.user !== undefined) { - logger.debug('user logout: ' + req.user.id) - } - } - req.logout() - res.redirect(config.serverURL + '/') -}) - -export { AuthRouter } diff --git a/old_src/lib/web/auth/interface.ts b/old_src/lib/web/auth/interface.ts deleted file mode 100644 index d6a7fade5..000000000 --- a/old_src/lib/web/auth/interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Router } from 'express' - -export interface AuthMiddleware { - getMiddleware (): Router; -} diff --git a/old_src/lib/web/auth/ldap/index.ts b/old_src/lib/web/auth/ldap/index.ts deleted file mode 100644 index 66b885ebf..000000000 --- a/old_src/lib/web/auth/ldap/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import LDAPStrategy from 'passport-ldapauth' - -import { config } from '../../../config' -import { errors } from '../../../errors' -import { logger } from '../../../logger' -import { User } from '../../../models' -import { urlencodedParser } from '../../utils' -import { AuthMiddleware } from '../interface' - -export const LdapMiddleware: AuthMiddleware = { - getMiddleware (): Router { - const LdapAuth = Router() - - passport.use(new LDAPStrategy({ - server: { - url: config.ldap.url || null, - bindDN: config.ldap.bindDn || null, - bindCredentials: config.ldap.bindCredentials || null, - searchBase: config.ldap.searchBase || null, - searchFilter: config.ldap.searchFilter || null, - searchAttributes: config.ldap.searchAttributes || null, - tlsOptions: config.ldap.tlsOptions || null, - starttls: config.ldap.starttls || null - } - }, function (user, done) { - let uuid = user.uidNumber || user.uid || user.sAMAccountName || undefined - if (config.ldap.useridField && user[config.ldap.useridField]) { - uuid = user[config.ldap.useridField] - } - - if (typeof uuid === 'undefined') { - throw new Error('Could not determine UUID for LDAP user. Check that ' + - 'either uidNumber, uid or sAMAccountName is set in your LDAP directory ' + - 'or use another unique attribute and configure it using the ' + - '"useridField" option in ldap settings.') - } - - let username = uuid - if (config.ldap.usernameField && user[config.ldap.usernameField]) { - username = user[config.ldap.usernameField] - } - - const profile = { - id: 'LDAP-' + uuid, - username: username, - displayName: user.displayName, - emails: user.mail ? Array.isArray(user.mail) ? user.mail : [user.mail] : [], - avatarUrl: null, - profileUrl: null, - provider: 'ldap' - } - const stringifiedProfile = JSON.stringify(profile) - User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile - } - }).then(function ([user, _]) { - if (user) { - let needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (needSave) { - user.save().then(function () { - logger.debug(`user login: ${user.id}`) - return done(null, user) - }) - } else { - logger.debug(`user login: ${user.id}`) - return done(null, user) - } - } - }).catch(function (err) { - logger.error('ldap auth failed: ' + err) - return done(err, null) - }) - })) - - LdapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) { - if (!req.body.username || !req.body.password) return errors.errorBadRequest(res) - passport.authenticate('ldapauth', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/', - failureFlash: true - })(req, res, next) - }) - - return LdapAuth - } -} diff --git a/old_src/lib/web/auth/oauth2/index.ts b/old_src/lib/web/auth/oauth2/index.ts deleted file mode 100644 index 8aa1d8f8d..000000000 --- a/old_src/lib/web/auth/oauth2/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' - -import { OAuth2CustomStrategy } from './oauth2-custom-strategy' -import { config } from '../../../config' -import { passportGeneralCallback } from '../utils' -import { AuthMiddleware } from '../interface' - -export const OAuth2Middleware: AuthMiddleware = { - getMiddleware (): Router { - const OAuth2Auth = Router() - - passport.use(new OAuth2CustomStrategy({ - authorizationURL: config.oauth2.authorizationURL, - tokenURL: config.oauth2.tokenURL, - clientID: config.oauth2.clientID, - clientSecret: config.oauth2.clientSecret, - callbackURL: config.serverURL + '/auth/oauth2/callback', - userProfileURL: config.oauth2.userProfileURL, - scope: config.oauth2.scope, - state: true - }, passportGeneralCallback)) - - OAuth2Auth.get('/auth/oauth2', passport.authenticate('oauth2')) - - // github auth callback - OAuth2Auth.get('/auth/oauth2/callback', - passport.authenticate('oauth2', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - return OAuth2Auth - } -} diff --git a/old_src/lib/web/auth/oauth2/oauth2-custom-strategy.ts b/old_src/lib/web/auth/oauth2/oauth2-custom-strategy.ts deleted file mode 100644 index 28b3c244c..000000000 --- a/old_src/lib/web/auth/oauth2/oauth2-custom-strategy.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { InternalOAuthError, Strategy as OAuth2Strategy } from 'passport-oauth2' -import { config } from '../../../config' -import { PassportProfile, ProviderEnum } from '../utils' -import { logger } from '../../../logger' - -function extractProfileAttribute (data, path: string): string { - // can handle stuff like `attrs[0].name` - const pathArray = path.split('.') - for (const segment of pathArray) { - const regex = /([\d\w]+)\[(.*)\]/ - const m = regex.exec(segment) - data = m ? data[m[1]][m[2]] : data[segment] - } - return data -} - -function parseProfile (data): Partial { - const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr) - let displayName: string | undefined - try { - // This may fail if the config.oauth2.userProfileDisplayNameAttr is undefined, - // or it is foo.bar and data["foo"] is undefined. - displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr) - } catch (e) { - displayName = undefined - logger.debug('\'id_token[%s]\' is undefined. Setting \'displayName\' to \'undefined\'.\n%s', config.oauth2.userProfileDisplayNameAttr, e.message) - } - - const emails: string[] = [] - try { - const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr) - if (email !== undefined) { - emails.push(email) - } else { - logger.debug('\'id_token[%s]\' is undefined. Setting \'emails\' to [].', config.oauth2.userProfileEmailAttr) - } - } catch (e) { - logger.debug('\'id_token[%s]\' is undefined. Setting \'emails\' to [].\n%s', config.oauth2.userProfileEmailAttr, e.message) - } - - return { - id: username, - username: username, - displayName: displayName, - emails: emails - } -} - -class OAuth2CustomStrategy extends OAuth2Strategy { - private readonly _userProfileURL: string; - - constructor (options, verify) { - options.customHeaders = options.customHeaders || {} - super(options, verify) - this.name = 'oauth2' - this._userProfileURL = options.userProfileURL - this._oauth2.useAuthorizationHeaderforGET(true) - } - - userProfile (accessToken, done): void { - this._oauth2.get(this._userProfileURL, accessToken, function (err, body, _) { - let json - - if (err) { - return done(new InternalOAuthError('Failed to fetch user profile', err)) - } - - try { - if (body !== undefined) { - json = JSON.parse(body.toString()) - } - } catch (ex) { - return done(new Error('Failed to parse user profile')) - } - - const profile = parseProfile(json) - profile.provider = ProviderEnum.oauth2 - - done(null, profile) - }) - } -} - -export { OAuth2CustomStrategy } diff --git a/old_src/lib/web/auth/openid/index.ts b/old_src/lib/web/auth/openid/index.ts deleted file mode 100644 index ebf7a5b70..000000000 --- a/old_src/lib/web/auth/openid/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import * as OpenID from '@passport-next/passport-openid' -import { config } from '../../../config' -import { User } from '../../../models' -import { logger } from '../../../logger' -import { urlencodedParser } from '../../utils' -import { AuthMiddleware } from '../interface' - -const openIDAuth = Router() -export const OPenIDMiddleware: AuthMiddleware = { - getMiddleware (): Router { - passport.use(new OpenID.Strategy({ - returnURL: config.serverURL + '/auth/openid/callback', - realm: config.serverURL, - profile: true - }, function (openid, profile, done) { - const stringifiedProfile = JSON.stringify(profile) - User.findOrCreate({ - where: { - profileid: openid - }, - defaults: { - profile: stringifiedProfile - } - }).then(function ([user, _]) { - if (user) { - let needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (needSave) { - user.save().then(function () { - logger.debug(`user login: ${user.id}`) - return done(null, user) - }) - } else { - logger.debug(`user login: ${user.id}`) - return done(null, user) - } - } - }).catch(function (err) { - logger.error('auth callback failed: ' + err) - return done(err, null) - }) - })) - openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) { - passport.authenticate('openid')(req, res, next) - }) - openIDAuth.get('/auth/openid/callback', - passport.authenticate('openid', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - return openIDAuth - } -} diff --git a/old_src/lib/web/auth/saml/index.ts b/old_src/lib/web/auth/saml/index.ts deleted file mode 100644 index 9e11e8ff5..000000000 --- a/old_src/lib/web/auth/saml/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import { Strategy as SamlStrategy } from 'passport-saml' -import fs from 'fs' - -import { config } from '../../../config' -import { User } from '../../../models' -import { logger } from '../../../logger' -import { urlencodedParser } from '../../utils' -import { AuthMiddleware } from '../interface' - -function intersection (array1: T[], array2: T[]): T[] { - return array1.filter((n) => array2.includes(n)) -} - -export const SamlMiddleware: AuthMiddleware = { - getMiddleware (): Router { - const SamlAuth = Router() - - const samlStrategy = new SamlStrategy({ - callbackUrl: config.serverURL + '/auth/saml/callback', - entryPoint: config.saml.idpSsoUrl, - issuer: config.saml.issuer || config.serverURL, - cert: fs.readFileSync(config.saml.idpCert, 'utf-8'), - identifierFormat: config.saml.identifierFormat, - disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext - }, function (user, done) { - // check authorization if needed - if (config.saml.externalGroups && config.saml.groupAttribute) { - const externalGroups: string[] = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) - if (externalGroups.length > 0) { - logger.error('saml permission denied: ' + externalGroups.join(', ')) - return done('Permission denied', null) - } - } - if (config.saml.requiredGroups && config.saml.groupAttribute) { - if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { - logger.error('saml permission denied') - return done('Permission denied', null) - } - } - // user creation - const uuid = user[config.saml.attribute.id] || user.nameID - const profile = { - provider: 'saml', - id: 'SAML-' + uuid, - username: user[config.saml.attribute.username] || user.nameID, - emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : [] - } - if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') { - profile.emails.push(user.nameID) - } - const stringifiedProfile = JSON.stringify(profile) - User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile - } - }).then(function ([user, _]) { - if (user) { - let needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (needSave) { - user.save().then(function () { - logger.debug(`user login: ${user.id}`) - return done(null, user) - }) - } else { - logger.debug(`user login: ${user.id}`) - return done(null, user) - } - } - }).catch(function (err) { - logger.error('saml auth failed: ' + err) - return done(err, null) - }) - }) - - passport.use(samlStrategy) - - SamlAuth.get('/auth/saml', - passport.authenticate('saml', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - SamlAuth.post('/auth/saml/callback', urlencodedParser, - passport.authenticate('saml', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - SamlAuth.get('/auth/saml/metadata', function (req, res) { - res.type('application/xml') - res.send(samlStrategy.generateServiceProviderMetadata(null)) - }) - - return SamlAuth - } -} diff --git a/old_src/lib/web/auth/twitter/index.ts b/old_src/lib/web/auth/twitter/index.ts deleted file mode 100644 index 84a0bbfda..000000000 --- a/old_src/lib/web/auth/twitter/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from 'express' -import passport from 'passport' -import { Strategy as TwitterStrategy } from 'passport-twitter' - -import { config } from '../../../config' -import { passportGeneralCallback } from '../utils' -import { AuthMiddleware } from '../interface' - -export const TwitterMiddleware: AuthMiddleware = { - getMiddleware (): Router { - const TwitterAuth = Router() - - passport.use(new TwitterStrategy({ - consumerKey: config.twitter.consumerKey, - consumerSecret: config.twitter.consumerSecret, - callbackURL: config.serverURL + '/auth/twitter/callback' - }, passportGeneralCallback)) - - TwitterAuth.get('/auth/twitter', function (req, res, next) { - passport.authenticate('twitter')(req, res, next) - }) - - // twitter auth callback - TwitterAuth.get('/auth/twitter/callback', - passport.authenticate('twitter', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) - ) - - return TwitterAuth - } -} diff --git a/old_src/lib/web/auth/utils.ts b/old_src/lib/web/auth/utils.ts deleted file mode 100644 index 4c957ef5c..000000000 --- a/old_src/lib/web/auth/utils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Profile } from 'passport' -import { logger } from '../../logger' -import { User } from '../../models' - -export function passportGeneralCallback ( - accessToken: string, - refreshToken: string, - profile: Profile, - done: (err?: Error | null, user?: User) => void -): void { - const stringifiedProfile = JSON.stringify(profile) - User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile, - accessToken: accessToken, - refreshToken: refreshToken - } - }).then(function ([user, _]) { - if (user) { - let needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (user.accessToken !== accessToken) { - user.accessToken = accessToken - needSave = true - } - if (user.refreshToken !== refreshToken) { - user.refreshToken = refreshToken - needSave = true - } - if (needSave) { - user.save().then(function () { - logger.debug(`user login: ${user.id}`) - return done(null, user) - }) - } else { - logger.debug(`user login: ${user.id}`) - return done(null, user) - } - } - }).catch(function (err) { - logger.error('auth callback failed: ' + err) - return done(err, undefined) - }) -} - -export enum ProviderEnum { - facebook = 'facebook', - twitter = 'twitter', - github = 'github', - gitlab = 'gitlab', - dropbox = 'dropbox', - google = 'google', - ldap = 'ldap', - oauth2 = 'oauth2', - saml = 'saml', -} - -export type PassportProfile = { - id: string; - username: string; - displayName: string; - emails: string[]; - avatarUrl: string; - profileUrl: string; - provider: ProviderEnum; - photos: { value: string }[]; -} diff --git a/old_src/lib/web/baseRouter.ts b/old_src/lib/web/baseRouter.ts deleted file mode 100644 index 5637a2a31..000000000 --- a/old_src/lib/web/baseRouter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { response } from '../response' -import { errors } from '../errors' -import { Router } from 'express' - -const BaseRouter = Router() - -// get index -BaseRouter.get('/', response.showIndex) -// get 403 forbidden -BaseRouter.get('/403', function (req, res) { - errors.errorForbidden(res) -}) -// get 404 not found -BaseRouter.get('/404', function (req, res) { - errors.errorNotFound(res) -}) -// get 500 internal error -BaseRouter.get('/500', function (req, res) { - errors.errorInternalError(res) -}) - -export { BaseRouter } diff --git a/old_src/lib/web/historyRouter.ts b/old_src/lib/web/historyRouter.ts deleted file mode 100644 index ef44636cd..000000000 --- a/old_src/lib/web/historyRouter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { urlencodedParser } from './utils' -import { History } from '../history' -import { Router } from 'express' - -const HistoryRouter = Router() - -// get history -HistoryRouter.get('/history', History.historyGet) -// post history -HistoryRouter.post('/history', urlencodedParser, History.historyPost) -// post history by note id -HistoryRouter.post('/history/:noteId', urlencodedParser, History.historyPost) -// delete history -HistoryRouter.delete('/history', History.historyDelete) -// delete history by note id -HistoryRouter.delete('/history/:noteId', History.historyDelete) - -export { HistoryRouter } diff --git a/old_src/lib/web/imageRouter/azure.ts b/old_src/lib/web/imageRouter/azure.ts deleted file mode 100644 index 9d2f0e1f1..000000000 --- a/old_src/lib/web/imageRouter/azure.ts +++ /dev/null @@ -1,38 +0,0 @@ -import azure from 'azure-storage' -import path from 'path' - -import { config } from '../../config' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -const AzureUploadProvider: UploadProvider = { - uploadImage: (imagePath, callback) => { - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - const azureBlobService = azure.createBlobService(config.azure.connectionString) - - azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, _, __) { - if (err) { - callback(new Error(err.message), undefined) - } else { - azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, _) { - if (err) { - callback(new Error(err.message), undefined) - } else { - callback(undefined, azureBlobService.getUrl(config.azure.container, result.name)) - } - }) - } - }) - } -} - -export { AzureUploadProvider } diff --git a/old_src/lib/web/imageRouter/filesystem.ts b/old_src/lib/web/imageRouter/filesystem.ts deleted file mode 100644 index a1b6b3fc8..000000000 --- a/old_src/lib/web/imageRouter/filesystem.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'path' -import { URL } from 'url' - -import { config } from '../../config' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -const FilesystemUploadProvider: UploadProvider = { - uploadImage: (imagePath, callback) => { - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - callback(undefined, (new URL(path.basename(imagePath), config.serverURL + '/uploads/')).href) - } -} - -export { FilesystemUploadProvider } diff --git a/old_src/lib/web/imageRouter/imgur.ts b/old_src/lib/web/imageRouter/imgur.ts deleted file mode 100644 index 384a63ff4..000000000 --- a/old_src/lib/web/imageRouter/imgur.ts +++ /dev/null @@ -1,30 +0,0 @@ -import imgur from 'old_src/lib/web/imageRouter/imgur' - -import { config } from '../../config' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -const ImgurUploadProvider: UploadProvider = { - uploadImage: (imagePath, callback) => { - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - imgur.setClientId(config.imgur.clientID) - imgur.uploadFile(imagePath) - .then(function (json) { - logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`) - callback(undefined, json.data.link.replace(/^http:\/\//i, 'https://')) - }).catch(function (err) { - callback(new Error(err), undefined) - }) - } -} - -export { ImgurUploadProvider } diff --git a/old_src/lib/web/imageRouter/index.ts b/old_src/lib/web/imageRouter/index.ts deleted file mode 100644 index 7ed46e8d1..000000000 --- a/old_src/lib/web/imageRouter/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Router } from 'express' -import formidable from 'formidable' - -import { config } from '../../config' -import { logger } from '../../logger' -import { errors } from '../../errors' -import { AzureUploadProvider } from './azure' -import { FilesystemUploadProvider } from './filesystem' -import { ImgurUploadProvider } from './imgur' -import { LutimUploadProvider } from './lutim' -import { MinioUploadProvider } from './minio' -import { S3UploadProvider } from './s3' - -interface UploadProvider { - uploadImage: (imagePath: string, callback: (err?: Error, url?: string) => void) => void; -} - -const ImageRouter = Router() - -// upload image -ImageRouter.post('/uploadimage', function (req, res) { - const form = new formidable.IncomingForm() - - form.keepExtensions = true - - if (config.imageUploadType === 'filesystem') { - form.uploadDir = config.uploadsPath - } - - form.parse(req, function (err, fields, files) { - if (err || !files.image || !files.image.path) { - logger.error(`formidable error: ${err}`) - errors.errorForbidden(res) - } else { - logger.debug(`SERVER received uploadimage: ${JSON.stringify(files.image)}`) - - let uploadProvider: UploadProvider - switch (config.imageUploadType) { - case 'azure': - uploadProvider = AzureUploadProvider - break - case 'filesystem': - default: - uploadProvider = FilesystemUploadProvider - break - case 'imgur': - uploadProvider = ImgurUploadProvider - break - case 'lutim': - uploadProvider = LutimUploadProvider - break - case 'minio': - uploadProvider = MinioUploadProvider - break - case 's3': - uploadProvider = S3UploadProvider - break - } - - logger.debug(`imageRouter: Uploading ${files.image.path} using ${config.imageUploadType}`) - uploadProvider.uploadImage(files.image.path, function (err, url) { - if (err !== undefined) { - logger.error(err) - return res.status(500).end('upload image error') - } - logger.debug(`SERVER sending ${url} to client`) - res.send({ - link: url - }) - }) - } - }) -}) - -export { ImageRouter, UploadProvider } diff --git a/old_src/lib/web/imageRouter/lutim.ts b/old_src/lib/web/imageRouter/lutim.ts deleted file mode 100644 index 71b6e70fe..000000000 --- a/old_src/lib/web/imageRouter/lutim.ts +++ /dev/null @@ -1,34 +0,0 @@ -import lutim from 'old_src/lib/web/imageRouter/lutim' - -import { config } from '../../config' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -const LutimUploadProvider: UploadProvider = { - uploadImage: (imagePath, callback) => { - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - if (config.lutim && config.lutim.url) { - lutim.setAPIUrl(config.lutim.url) - logger.debug(`Set lutim URL to ${lutim.getAPIUrl()}`) - } - - lutim.uploadImage(imagePath) - .then(function (json) { - logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`) - callback(undefined, lutim.getAPIUrl() + json.msg.short) - }).catch(function (err) { - callback(new Error(err), undefined) - }) - } -} - -export { LutimUploadProvider } diff --git a/old_src/lib/web/imageRouter/minio.ts b/old_src/lib/web/imageRouter/minio.ts deleted file mode 100644 index b69c20a0c..000000000 --- a/old_src/lib/web/imageRouter/minio.ts +++ /dev/null @@ -1,60 +0,0 @@ -import path from 'path' -import fs from 'fs' -import { Client } from 'old_src/lib/web/imageRouter/minio' - -import { config } from '../../config' -import { getImageMimeType } from '../../utils/functions' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -let MinioUploadProvider: UploadProvider - -if (config.minio.endPoint !== undefined) { - const minioClient = new Client({ - endPoint: config.minio.endPoint, - port: config.minio.port, - useSSL: config.minio.secure, - accessKey: config.minio.accessKey, - secretKey: config.minio.secretKey - }) - - MinioUploadProvider = { - uploadImage: (imagePath, callback): void => { - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - fs.readFile(imagePath, function (err, buffer) { - if (err) { - callback(new Error(err.message), undefined) - return - } - - const key = path.join('uploads', path.basename(imagePath)) - const protocol = config.minio.secure ? 'https' : 'http' - - const metaData = { - ContentType: getImageMimeType(imagePath) - } - - minioClient.putObject(config.s3bucket, key, buffer, buffer.length, metaData, function (err, _) { - if (err) { - callback(new Error(err.message), undefined) - return - } - const hidePort = [80, 443].includes(config.minio.port) - const urlPort = hidePort ? '' : `:${config.minio.port}` - callback(undefined, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`) - }) - }) - } - } -} - -export { MinioUploadProvider } diff --git a/old_src/lib/web/imageRouter/s3.ts b/old_src/lib/web/imageRouter/s3.ts deleted file mode 100644 index 4c58ff488..000000000 --- a/old_src/lib/web/imageRouter/s3.ts +++ /dev/null @@ -1,59 +0,0 @@ -import fs from 'fs' -import path from 'path' -import AWS from 'aws-sdk' - -import { config } from '../../config' -// import { getImageMimeType } from '../../utils' -import { logger } from '../../logger' -import { UploadProvider } from './index' - -const awsConfig = new AWS.Config(config.s3) -const s3 = new AWS.S3(awsConfig) - -const S3UploadProvider: UploadProvider = { - uploadImage: (imagePath, callback) => { - if (!imagePath) { - callback(new Error('Image path is missing or wrong'), undefined) - return - } - - if (!callback || typeof callback !== 'function') { - logger.error('Callback has to be a function') - return - } - - fs.readFile(imagePath, function (err, buffer) { - if (err) { - callback(new Error(err.message), undefined) - return - } - const params = { - Bucket: config.s3bucket, - Key: path.join('uploads', path.basename(imagePath)), - Body: buffer - } - - // ToDo: This does not exist (anymore?) - // const mimeType = getImageMimeType(imagePath) - // if (mimeType) { params.ContentType = mimeType } - - logger.debug(`S3 object parameters: ${JSON.stringify(params)}`) - s3.putObject(params, function (err, _) { - if (err) { - callback(new Error(err.message), undefined) - return - } - - let s3Endpoint = 's3.amazonaws.com' - if (config.s3.endpoint) { - s3Endpoint = config.s3.endpoint - } else if (config.s3.region && config.s3.region !== 'us-east-1') { - s3Endpoint = `s3-${config.s3.region}.amazonaws.com` - } - callback(undefined, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`) - }) - }) - } -} - -export { S3UploadProvider } diff --git a/old_src/lib/web/index.ts b/old_src/lib/web/index.ts deleted file mode 100644 index 2d3f962c3..000000000 --- a/old_src/lib/web/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AuthRouter } from './auth' -import { BaseRouter } from './baseRouter' -import { HistoryRouter } from './historyRouter' -import { ImageRouter } from './imageRouter' -import { NoteRouter } from './note/router' -import { StatusRouter } from './statusRouter' -import { UserRouter } from './userRouter' - -export { AuthRouter, BaseRouter, HistoryRouter, ImageRouter, NoteRouter, StatusRouter, UserRouter } diff --git a/old_src/lib/web/middleware/checkURIValid.ts b/old_src/lib/web/middleware/checkURIValid.ts deleted file mode 100644 index 462b3e9e3..000000000 --- a/old_src/lib/web/middleware/checkURIValid.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { logger } from '../../logger' -import { errors } from '../../errors' -import { NextFunction, Request, Response } from 'express' - -export function checkURI (req: Request, res: Response, next: NextFunction): void { - try { - decodeURIComponent(req.path) - next() - } catch (err) { - logger.error(err) - errors.errorBadRequest(res) - } -} diff --git a/old_src/lib/web/middleware/codiMDVersion.ts b/old_src/lib/web/middleware/codiMDVersion.ts deleted file mode 100644 index e10663a4b..000000000 --- a/old_src/lib/web/middleware/codiMDVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { config } from '../../config' -import { NextFunction, Request, Response } from 'express' - -export function codiMDVersion (req: Request, res: Response, next: NextFunction): void { - res.set({ - 'CodiMD-Version': config.version - }) - return next() -} diff --git a/old_src/lib/web/middleware/index.ts b/old_src/lib/web/middleware/index.ts deleted file mode 100644 index 0943be063..000000000 --- a/old_src/lib/web/middleware/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { checkURI } from './checkURIValid' -import { codiMDVersion } from './codiMDVersion' -import { redirectWithoutTrailingSlashes } from './redirectWithoutTrailingSlashes' -import { tooBusy } from './tooBusy' - -export { checkURI, codiMDVersion, redirectWithoutTrailingSlashes, tooBusy } diff --git a/old_src/lib/web/middleware/redirectWithoutTrailingSlashes.ts b/old_src/lib/web/middleware/redirectWithoutTrailingSlashes.ts deleted file mode 100644 index 129786b53..000000000 --- a/old_src/lib/web/middleware/redirectWithoutTrailingSlashes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextFunction, Request, Response } from 'express' -import { config } from '../../config' - -export function redirectWithoutTrailingSlashes (req: Request, res: Response, next: NextFunction): void { - if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { - const queryString: string = req.url.slice(req.path.length) - const urlPath: string = req.path.slice(0, -1) - let serverURL: string = config.serverURL - if (config.urlPath) { - serverURL = serverURL.slice(0, -(config.urlPath.length + 1)) - } - res.redirect(301, serverURL + urlPath + queryString) - } else { - next() - } -} diff --git a/old_src/lib/web/middleware/tooBusy.ts b/old_src/lib/web/middleware/tooBusy.ts deleted file mode 100644 index c81ff9468..000000000 --- a/old_src/lib/web/middleware/tooBusy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import toobusy from 'toobusy-js' -import { errors } from '../../errors' -import { config } from '../../config' -import { NextFunction, Request, Response } from 'express' - -toobusy.maxLag(config.tooBusyLag) - -export function tooBusy (req: Request, res: Response, next: NextFunction): void { - if (toobusy()) { - errors.errorServiceUnavailable(res) - } else { - next() - } -} diff --git a/old_src/lib/web/note/actions.ts b/old_src/lib/web/note/actions.ts deleted file mode 100644 index a1f3bfa11..000000000 --- a/old_src/lib/web/note/actions.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Request, Response } from 'express' - -import { Note, Revision } from '../../models' -import { logger } from '../../logger' -import { config } from '../../config' -import { errors } from '../../errors' -import shortId from 'shortid' -import moment from 'moment' -import querystring from 'querystring' - -export function getInfo (req: Request, res: Response, note: Note): void { - const body = note.content - const extracted = Note.extractMeta(body) - const markdown = extracted.markdown - const meta = Note.parseMeta(extracted.meta) - const title = Note.decodeTitle(note.title) - const data = { - title: meta.title || title, - description: meta.description || (markdown ? Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: note.createdAt, - updatetime: note.lastchangeAt - } - res.set({ - 'Access-Control-Allow-Origin': '*', // allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }) - res.send(data) -} - -export function createGist (req: Request, res: Response, note: Note): void { - const data = { - // eslint-disable-next-line @typescript-eslint/camelcase - client_id: config.github.clientID, - // eslint-disable-next-line @typescript-eslint/camelcase - redirect_uri: config.serverURL + '/auth/github/callback/' + Note.encodeNoteId(note.id) + '/gist', - scope: 'gist', - state: shortId.generate() - } - const query = querystring.stringify(data) - res.redirect('https://github.com/login/oauth/authorize?' + query) -} - -export function getRevision (req: Request, res: Response, note: Note): void { - const actionId = req.params.actionId - if (actionId) { - const time = moment(parseInt(actionId)) - if (time.isValid()) { - Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) { - if (err) { - logger.error(err) - errors.errorInternalError(res) - - return - } - if (!content) { - errors.errorNotFound(res) - return - } - res.set({ - 'Access-Control-Allow-Origin': '*', // allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }) - res.send(content) - }) - } else { - errors.errorNotFound(res) - } - } else { - Revision.getNoteRevisions(note, function (err, data) { - if (err) { - logger.error(err) - errors.errorInternalError(res) - return - } - const out = { - revision: data - } - res.set({ - 'Access-Control-Allow-Origin': '*', // allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }) - res.send(out) - }) - } -} diff --git a/old_src/lib/web/note/controller.ts b/old_src/lib/web/note/controller.ts deleted file mode 100644 index ec024123d..000000000 --- a/old_src/lib/web/note/controller.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Request, Response } from 'express' -import { config } from '../../config' -import { errors } from '../../errors' -import { logger } from '../../logger' -import { Note } from '../../models' -import * as ActionController from './actions' -import * as NoteUtils from './util' - -export function publishNoteActions (req: Request, res: Response): void { - NoteUtils.findNoteOrCreate(req, res, function (note) { - const action = req.params.action - switch (action) { - case 'download': - exports.downloadMarkdown(req, res, note) - break - case 'edit': - res.redirect(config.serverURL + '/' + (note.alias ? note.alias : Note.encodeNoteId(note.id)) + '?both') - break - default: - res.redirect(config.serverURL + '/s/' + note.shortid) - break - } - }) -} - -export function showPublishNote (req: Request, res: Response): void { - NoteUtils.findNoteOrCreate(req, res, function (note) { - // force to use short id - const shortid = req.params.shortid - if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { - return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid)) - } - note.increment('viewcount').then(function (note) { - if (!note) { - return errors.errorNotFound(res) - } - NoteUtils.getPublishData(req, res, note, (data) => { - res.set({ - 'Cache-Control': 'private' // only cache by client - }) - return res.render('pretty.ejs', data) - }) - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) - }) -} - -export function showNote (req: Request, res: Response): void { - NoteUtils.findNoteOrCreate(req, res, function (note) { - // force to use note id - const noteId = req.params.noteId - const id = Note.encodeNoteId(note.id) - if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) { - return res.redirect(config.serverURL + '/' + (note.alias || id)) - } - const body = note.content - const extracted = Note.extractMeta(body) - const meta = Note.parseMeta(extracted.meta) - let title = Note.decodeTitle(note.title) - title = Note.generateWebTitle(meta.title || title) - const opengraph = Note.parseOpengraph(meta, title) - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }) - return res.render('codimd.ejs', { - title: title, - opengraph: opengraph - }) - }) -} - -export function createFromPOST (req: Request, res: Response): void { - let body = '' - if (req.body && req.body.length > config.documentMaxLength) { - return errors.errorTooLong(res) - } else if (req.body) { - body = req.body - } - body = body.replace(/[\r]/g, '') - return NoteUtils.newNote(req, res, body) -} - -export function doAction (req: Request, res: Response): void { - const noteId = req.params.noteId - NoteUtils.findNoteOrCreate(req, res, (note) => { - const action = req.params.action - // TODO: Don't switch on action, choose action in Router and use separate functions - switch (action) { - case 'publish': - case 'pretty': // pretty deprecated - res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid)) - break - case 'slide': - res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid)) - break - case 'download': - exports.downloadMarkdown(req, res, note) - break - case 'info': - ActionController.getInfo(req, res, note) - break - case 'gist': - ActionController.createGist(req, res, note) - break - case 'revision': - ActionController.getRevision(req, res, note) - break - default: - return res.redirect(config.serverURL + '/' + noteId) - } - }) -} - -export function downloadMarkdown (req: Request, res: Response, note): void { - const body = note.content - let filename = Note.decodeTitle(note.title) - filename = encodeURIComponent(filename) - res.set({ - 'Access-Control-Allow-Origin': '*', // allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Content-Type': 'text/markdown; charset=UTF-8', - 'Cache-Control': 'private', - 'Content-disposition': 'attachment; filename=' + filename + '.md', - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }) - res.send(body) -} diff --git a/old_src/lib/web/note/router.ts b/old_src/lib/web/note/router.ts deleted file mode 100644 index 034d89a16..000000000 --- a/old_src/lib/web/note/router.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Router } from 'express' -import { markdownParser } from '../utils' -import * as NoteController from './controller' - -import * as SlideController from './slide' - -const NoteRouter = Router() -// get new note -NoteRouter.get('/new', NoteController.createFromPOST) -// post new note with content -NoteRouter.post('/new', markdownParser, NoteController.createFromPOST) -// post new note with content and alias -NoteRouter.post('/new/:noteId', markdownParser, NoteController.createFromPOST) -// get publish note -NoteRouter.get('/s/:shortid', NoteController.showPublishNote) -// publish note actions -NoteRouter.get('/s/:shortid/:action', NoteController.publishNoteActions) -// get publish slide -NoteRouter.get('/p/:shortid', SlideController.showPublishSlide) -// publish slide actions -NoteRouter.get('/p/:shortid/:action', SlideController.publishSlideActions) -// get note by id -NoteRouter.get('/:noteId', NoteController.showNote) -// note actions -NoteRouter.get('/:noteId/:action', NoteController.doAction) -// note actions with action id -NoteRouter.get('/:noteId/:action/:actionId', NoteController.doAction) - -export { NoteRouter } diff --git a/old_src/lib/web/note/slide.ts b/old_src/lib/web/note/slide.ts deleted file mode 100644 index 2172f59a3..000000000 --- a/old_src/lib/web/note/slide.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Request, Response } from 'express' -import { config } from '../../config' -import { errors } from '../../errors' -import { logger } from '../../logger' -import { Note } from '../../models' -import * as NoteUtils from './util' - -export function publishSlideActions (req: Request, res: Response): void { - NoteUtils.findNoteOrCreate(req, res, function (note) { - const action = req.params.action - if (action === 'edit') { - res.redirect(config.serverURL + '/' + (note.alias ? note.alias : Note.encodeNoteId(note.id)) + '?both') - } else { - res.redirect(config.serverURL + '/p/' + note.shortid) - } - }) -} - -export function showPublishSlide (req: Request, res: Response): void { - NoteUtils.findNoteOrCreate(req, res, function (note) { - // force to use short id - const shortid = req.params.shortid - if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { - return res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid)) - } - note.increment('viewcount').then(function (note) { - if (!note) { - return errors.errorNotFound(res) - } - NoteUtils.getPublishData(req, res, note, (data) => { - res.set({ - 'Cache-Control': 'private' // only cache by client - }) - return res.render('slide.ejs', data) - }) - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) - }) -} diff --git a/old_src/lib/web/note/util.ts b/old_src/lib/web/note/util.ts deleted file mode 100644 index 7e470cd5a..000000000 --- a/old_src/lib/web/note/util.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Request, Response } from 'express' -import fs from 'fs' - -import path from 'path' -import { config } from '../../config' -import { errors } from '../../errors' -import { logger } from '../../logger' -import { Note } from '../../models' -import { PhotoProfile } from '../../utils/PhotoProfile' - -export function newNote (req, res: Response, body: string | null): void { - let owner = null - const noteId = req.params.noteId ? req.params.noteId : null - if (req.isAuthenticated()) { - owner = req.user.id - } else if (!config.allowAnonymous) { - return errors.errorForbidden(res) - } - if (config.allowFreeURL && noteId && !config.forbiddenNoteIDs.includes(noteId)) { - req.alias = noteId - } else if (noteId) { - return req.method === 'POST' ? errors.errorForbidden(res) : errors.errorNotFound(res) - } - Note.create({ - ownerId: owner, - alias: req.alias ? req.alias : null, - content: body - }).then(function (note) { - return res.redirect(config.serverURL + '/' + (note.alias ? note.alias : Note.encodeNoteId(note.id))) - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) -} - -export enum Permission { - None, - Read, - Write, - Owner -} - -interface NoteObject { - ownerId?: string; - permission: string; -} - -export function getPermission (user, note: NoteObject): Permission { - // There are two possible User objects we get passed. One is from socket.io - // and the other is from passport directly. The former sets the logged_in - // parameter to either true or false, whereas for the latter, the logged_in - // parameter is always undefined, and the existence of user itself means the - // user is logged in. - if (!user || user.logged_in === false) { - // Anonymous - switch (note.permission) { - case 'freely': - return Permission.Write - case 'editable': - case 'locked': - return Permission.Read - default: - return Permission.None - } - } else if (note.ownerId === user.id) { - // Owner - return Permission.Owner - } else { - // Registered user - switch (note.permission) { - case 'editable': - case 'limited': - case 'freely': - return Permission.Write - case 'locked': - case 'protected': - return Permission.Read - default: - return Permission.None - } - } -} - -export function findNoteOrCreate (req: Request, res: Response, callback: (note: Note) => void): void { - const id = req.params.noteId || req.params.shortid - Note.parseNoteId(id, function (err, _id) { - if (err) { - logger.error(err) - return errors.errorInternalError(res) - } - Note.findOne({ - where: { - id: _id - } - }).then(function (note) { - if (!note) { - return newNote(req, res, '') - } - if (getPermission(req.user, note) === Permission.None) { - return errors.errorForbidden(res) - } else { - return callback(note) - } - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) - }) -} - -function isRevealTheme (theme: string): string | undefined { - if (fs.existsSync(path.join(__dirname, '..', '..', '..', '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) { - return theme - } - return undefined -} - -export function getPublishData (req: Request, res: Response, note, callback: (data) => void): void { - const body = note.content - const extracted = Note.extractMeta(body) - const markdown = extracted.markdown - const meta = Note.parseMeta(extracted.meta) - const createtime = note.createdAt - const updatetime = note.lastchangeAt - let title = Note.decodeTitle(note.title) - title = Note.generateWebTitle(meta.title || title) - const ogdata = Note.parseOpengraph(meta, title) - const data = { - title: title, - description: meta.description || (markdown ? Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime, - body: markdown, - theme: meta.slideOptions && isRevealTheme(meta.slideOptions.theme), - meta: JSON.stringify(extracted.meta), - owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? PhotoProfile.fromUser(note.owner) : null, - lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? PhotoProfile.fromUser(note.lastchangeuser) : null, - robots: meta.robots || false, // default allow robots - GA: meta.GA, - disqus: meta.disqus, - cspNonce: res.locals.nonce, - dnt: req.headers.dnt, - opengraph: ogdata - } - callback(data) -} diff --git a/old_src/lib/web/statusRouter.ts b/old_src/lib/web/statusRouter.ts deleted file mode 100644 index 7eac7721c..000000000 --- a/old_src/lib/web/statusRouter.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { config } from '../config' -import { Router } from 'express' -import { errors } from '../errors' -import { realtime } from '../realtime' -import { Temp } from '../models' -import { logger } from '../logger' -import { urlencodedParser } from './utils' - -const StatusRouter = Router() - -// get status -StatusRouter.get('/status', function (req, res, _) { - realtime.getStatus(function (data) { - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling - 'Content-Type': 'application/json' - }) - res.send(data) - }) -}) -// get status -StatusRouter.get('/temp', function (req, res) { - const host = req.get('host') - if (config.allowOrigin.indexOf(host) === -1) { - errors.errorForbidden(res) - } else { - const tempid = req.query.tempid - if (!tempid || typeof tempid !== 'string') { - errors.errorForbidden(res) - } else { - Temp.findOne({ - where: { - id: tempid - } - }).then(function (temp) { - if (!temp) { - errors.errorNotFound(res) - } else { - res.header('Access-Control-Allow-Origin', '*') - res.send({ - temp: temp.data - }) - temp.destroy().catch(function (err) { - if (err) { - logger.error('remove temp failed: ' + err) - } - }) - } - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) - } - } -}) -// post status -StatusRouter.post('/temp', urlencodedParser, function (req, res) { - const host = req.get('host') - if (config.allowOrigin.indexOf(host) === -1) { - errors.errorForbidden(res) - } else { - const data = req.body.data - if (!data) { - errors.errorForbidden(res) - } else { - logger.debug(`SERVER received temp from [${host}]: ${req.body.data}`) - Temp.create({ - data: data - }).then(function (temp) { - if (temp) { - res.header('Access-Control-Allow-Origin', '*') - res.send({ - status: 'ok', - id: temp.id - }) - } else { - errors.errorInternalError(res) - } - }).catch(function (err) { - logger.error(err) - return errors.errorInternalError(res) - }) - } - } -}) - -StatusRouter.get('/config', function (req, res) { - const data = { - domain: config.domain, - urlpath: config.urlPath, - debug: config.debug, - version: config.fullversion, - DROPBOX_APP_KEY: config.dropbox.appKey, - allowedUploadMimeTypes: config.allowedUploadMimeTypes, - linkifyHeaderStyle: config.linkifyHeaderStyle - } - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling - 'Content-Type': 'application/javascript' - }) - res.render('../js/lib/common/constant.ejs', data) -}) - -export { StatusRouter } diff --git a/old_src/lib/web/userRouter.ts b/old_src/lib/web/userRouter.ts deleted file mode 100644 index 819573511..000000000 --- a/old_src/lib/web/userRouter.ts +++ /dev/null @@ -1,144 +0,0 @@ -import archiver from 'archiver' -import async from 'async' -import { Request, Response, Router } from 'express' -import { errors } from '../errors' -import { Note, User } from '../models' -import { logger } from '../logger' -import { generateAvatar } from '../letter-avatars' -import { config } from '../config' -import { PhotoProfile } from '../utils/PhotoProfile' - -const UserRouter = Router() - -// get me info -UserRouter.get('/me', function (req: Request, res: Response) { - if (req.isAuthenticated()) { - if (req.user == null) { - return errors.errorInternalError(res) - } - User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) { return errors.errorNotFound(res) } - const profile = PhotoProfile.fromUser(user) - if (profile == null) { - return errors.errorInternalError(res) - } - res.send({ - status: 'ok', - id: user.id, - name: profile.name, - photo: profile.photo - }) - }).catch(function (err) { - logger.error('read me failed: ' + err) - return errors.errorInternalError(res) - }) - } else { - res.send({ - status: 'forbidden' - }) - } -}) - -// delete the currently authenticated user -UserRouter.get('/me/delete/:token?', function (req: Request, res: Response) { - if (req.isAuthenticated()) { - if (req.user == null) { - return errors.errorInternalError(res) - } - User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) { - return errors.errorNotFound(res) - } - if (user.deleteToken === req.params.token) { - user.destroy().then(function () { - res.redirect(config.serverURL + '/') - }) - } else { - return errors.errorForbidden(res) - } - }).catch(function (err) { - logger.error('delete user failed: ' + err) - return errors.errorInternalError(res) - }) - } else { - return errors.errorForbidden(res) - } -}) - -// export the data of the authenticated user -UserRouter.get('/me/export', function (req: Request, res: Response) { - if (req.isAuthenticated()) { - if (req.user == null) { - return errors.errorInternalError(res) - } - // let output = fs.createWriteStream(__dirname + '/example.zip'); - const 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 errors.errorInternalError(res) - }) - User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (user == null) { - return errors.errorInternalError(res) - } - Note.findAll({ - where: { - ownerId: user.id - } - }).then(function (notes) { - const filenames = {} - async.each(notes, function (note, callback) { - const basename = note.title.replace(/\//g, '-') // Prevent subdirectories - let filename - let numberOfDuplicateFilename = 0 - do { - const suffix = numberOfDuplicateFilename !== 0 ? '-' + numberOfDuplicateFilename : '' - filename = basename + suffix + '.md' - numberOfDuplicateFilename++ - } while (filenames[filename]) - filenames[filename] = true - - logger.debug('Write: ' + filename) - archive.append(Buffer.from(note.content), { name: filename, date: note.lastchangeAt }) - callback(null, null) - }, function (err) { - if (err) { - return errors.errorInternalError(res) - } - - archive.finalize() - }) - }) - }).catch(function (err) { - logger.error('export user data failed: ' + err) - return errors.errorInternalError(res) - }) - } else { - return errors.errorForbidden(res) - } -}) - -UserRouter.get('/user/:username/avatar.svg', function (req: Request, res: Response, _) { - res.setHeader('Content-Type', 'image/svg+xml') - res.setHeader('Cache-Control', 'public, max-age=86400') - res.send(generateAvatar(req.params.username)) -}) - -export { UserRouter } diff --git a/old_src/lib/web/utils.ts b/old_src/lib/web/utils.ts deleted file mode 100644 index 6da5ceb99..000000000 --- a/old_src/lib/web/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import bodyParser from 'body-parser' -// create application/x-www-form-urlencoded parser - -export const urlencodedParser = bodyParser.urlencoded({ - extended: false, - limit: 1024 * 1024 * 10 // 10 mb -}) - -// create text/markdown parser -export const markdownParser = bodyParser.text({ - inflate: true, - type: ['text/plain', 'text/markdown'], - limit: 1024 * 1024 * 10 // 10 mb -}) diff --git a/old_src/lib/workers/dmpWorker.ts b/old_src/lib/workers/dmpWorker.ts deleted file mode 100644 index 14449aa89..000000000 --- a/old_src/lib/workers/dmpWorker.ts +++ /dev/null @@ -1,173 +0,0 @@ -// external modules -// eslint-disable-next-line @typescript-eslint/camelcase -import { DIFF_DELETE, DIFF_INSERT, diff_match_patch, patch_obj } from 'diff-match-patch' -import { Revision } from '../models' - -// Function for suppressing TS2722 -// eslint-disable-next-line @typescript-eslint/unbound-method,@typescript-eslint/no-empty-function -function processSend (options): boolean { - if (process?.send !== undefined) { - return process.send(options) - } - return false -} - -// We can't use the logger directly, because we are in a distinct nodejs -// process and the global instance of logger is not the one of the parent. In -// particular, it does not have the log level set correctly. -function log (level: string, msg, ...splat): boolean { - return processSend({ - msg: 'log', - level: level, - result: [msg, splat], - cacheKey: 1 // dummy value - }) -} -// eslint-disable-next-line @typescript-eslint/camelcase,new-cap -const dmp: diff_match_patch = new diff_match_patch() - -// eslint-disable-next-line @typescript-eslint/camelcase -function getRevision (revisions: Revision[], count: number): { content: string; patch: patch_obj[]; authorship: string } { - const msStart = (new Date()).getTime() - let startContent = '' - let lastPatch = '' - // eslint-disable-next-line @typescript-eslint/camelcase - let applyPatches: patch_obj[] = [] - let authorship = '' - if (count <= Math.round(revisions.length / 2)) { - // start from top to target - for (let i = 0; i < count; i++) { - const revision = revisions[i] - if (i === 0) { - startContent = revision.content || revision.lastContent - } - if (i !== count - 1) { - // eslint-disable-next-line @typescript-eslint/camelcase - const patch: patch_obj[] = dmp.patch_fromText(revision.patch) - applyPatches = applyPatches.concat(patch) - } - lastPatch = revision.patch - authorship = revision.authorship - } - // swap DIFF_INSERT and DIFF_DELETE to achieve unpatching - for (let i = 0, l = applyPatches.length; i < l; i++) { - for (let j = 0, m = applyPatches[i].diffs.length; j < m; j++) { - const diff = applyPatches[i].diffs[j] - if (diff[0] === DIFF_INSERT) { - diff[0] = DIFF_DELETE - } else if (diff[0] === DIFF_DELETE) { - diff[0] = DIFF_INSERT - } - } - } - } else { - // start from bottom to target - const l = revisions.length - 1 - for (let i = l; i >= count - 1; i--) { - const revision = revisions[i] - if (i === l) { - startContent = revision.lastContent - authorship = revision.authorship - } - if (revision.patch) { - // eslint-disable-next-line @typescript-eslint/camelcase - const patch: patch_obj[] = dmp.patch_fromText(revision.patch) - applyPatches = applyPatches.concat(patch) - } - lastPatch = revision.patch - authorship = revision.authorship - } - } - let finalContent = '' - try { - finalContent = dmp.patch_apply(applyPatches, startContent)[0] - } catch (err) { - throw new Error(err) - } - const data = { - content: finalContent, - patch: dmp.patch_fromText(lastPatch), - authorship: authorship - } - const msEnd = (new Date()).getTime() - log('debug', (msEnd - msStart) + 'ms') - return data -} - -function createPatch (lastDoc: string, currDoc: string): string { - const msStart = (new Date()).getTime() - const diff = dmp.diff_main(lastDoc, currDoc) - // eslint-disable-next-line @typescript-eslint/camelcase - const patch: patch_obj[] = dmp.patch_make(lastDoc, diff) - const strPatch: string = dmp.patch_toText(patch) - const msEnd = (new Date()).getTime() - log('debug', strPatch) - log('debug', (msEnd - msStart) + 'ms') - return strPatch -} - -class Data { - msg: string - cacheKey: string - lastDoc?: string - currDoc?: string - revisions?: Revision[] - count?: number -} - -process.on('message', function (data: Data) { - if (!data || !data.msg || !data.cacheKey) { - return log('error', 'dmp worker error: not enough data') - } - switch (data.msg) { - case 'create patch': - if (data.lastDoc === undefined || data.currDoc === undefined) { - return log('error', 'dmp worker error: not enough data on create patch') - } - try { - const patch: string = createPatch(data.lastDoc, data.currDoc) - processSend({ - msg: 'check', - result: patch, - cacheKey: data.cacheKey - }) - } catch (err) { - log('error', 'create patch: dmp worker error', err) - processSend({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }) - } - break - case 'get revision': - if (data.revisions === undefined || data.count === undefined) { - return log('error', 'dmp worker error: not enough data on get revision') - } - try { - // eslint-disable-next-line @typescript-eslint/camelcase - const result: { content: string; patch: patch_obj[]; authorship: string } = getRevision(data.revisions, data.count) - processSend({ - msg: 'check', - result: result, - cacheKey: data.cacheKey - }) - } catch (err) { - log('error', 'get revision: dmp worker error', err) - processSend({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }) - } - break - } -}) - -// log uncaught exception -process.on('uncaughtException', function (err: Error) { - log('error', 'An uncaught exception has occured.') - log('error', err) - log('error', 'Process will exit now.') - process.exit(1) -}) diff --git a/old_src/test/auth.ts b/old_src/test/auth.ts deleted file mode 100644 index 334206307..000000000 --- a/old_src/test/auth.ts +++ /dev/null @@ -1,110 +0,0 @@ -import assert from 'assert' -import { ImportMock } from 'ts-mock-imports' -import * as configModule from '../lib/config' -import { DropboxMiddleware } from '../lib/web/auth/dropbox' -import { EmailMiddleware } from '../lib/web/auth/email' -import { FacebookMiddleware } from '../lib/web/auth/facebook' -import { GithubMiddleware } from '../lib/web/auth/github' -import { GitlabMiddleware } from '../lib/web/auth/gitlab' -import { GoogleMiddleware } from '../lib/web/auth/google' -import { LdapMiddleware } from '../lib/web/auth/ldap' -import { OAuth2Middleware } from '../lib/web/auth/oauth2' -import { OPenIDMiddleware } from '../lib/web/auth/openid' -import { TwitterMiddleware } from '../lib/web/auth/twitter' - -describe('AuthMiddlewares', function () { - // We currently exclude the SAML Auth, because it needs a certificate file - const middlewareList = [{ - name: 'Facebook', - middleware: FacebookMiddleware, - config: { - facebook: { - clientID: 'foobar', - clientSecret: 'foobar' - } - } - }, { - name: 'Twitter', - middleware: TwitterMiddleware, - config: { - twitter: { - consumerKey: 'foobar', - consumerSecret: 'foobar' - } - } - }, { - name: 'GitHub', - middleware: GithubMiddleware, - config: { - github: { - clientID: 'foobar', - clientSecret: 'foobar' - } - } - }, { - name: 'Gitlab', - middleware: GitlabMiddleware, - config: { - gitlab: { - clientID: 'foobar', - clientSecret: 'foobar' - } - } - }, { - name: 'Dropbox', - middleware: DropboxMiddleware, - config: { - dropbox: { - clientID: 'foobar', - clientSecret: 'foobar' - } - } - }, { - name: 'Google', - middleware: GoogleMiddleware, - config: { - google: { - clientID: 'foobar', - clientSecret: 'foobar' - } - } - }, { - name: 'LDAP', - middleware: LdapMiddleware, - config: { - ldap: {} - } - }, { - name: 'OAuth2', - middleware: OAuth2Middleware, - config: { - oauth2: { - clientID: 'foobar', - clientSecret: 'foobar', - authorizationURL: 'foobar', - tokenURL: 'foobar', - userProfileURL: 'foobar', - scope: 'foobar' - } - } - }, { - name: 'Email', - middleware: EmailMiddleware, - config: {} - }, { - name: 'OpenID', - middleware: OPenIDMiddleware, - config: {} - }] - - middlewareList.forEach((middleware) => { - describe(middleware.name + 'Middleware', () => { - before(() => { - ImportMock.mockOther(configModule, 'config', middleware.config) - }) - it('can be instantiated', () => { - assert.ok(middleware.middleware.getMiddleware()) - }) - }) - }) -}) diff --git a/old_src/test/csp.ts b/old_src/test/csp.ts deleted file mode 100644 index 52a4b6789..000000000 --- a/old_src/test/csp.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-env node, mocha */ -'use strict' - -import assert from 'assert' -import crypto from 'crypto' -import fs from 'fs' -import path from 'path' -import * as configModule from '../lib/config' -import { ImportMock } from 'ts-mock-imports' - -describe('Content security policies', function () { - let defaultConfig, csp - - before(function () { - csp = require('../lib/csp') - }) - - beforeEach(function () { - // Reset config to make sure we don't influence other tests - defaultConfig = { - csp: { - enable: true, - directives: { - }, - addDefaults: true, - addDisqus: true, - addGoogleAnalytics: true, - upgradeInsecureRequests: 'auto', - reportURI: undefined - }, - useCDN: true - } - }) - - // beginnging Tests - it('Disable CDN', function () { - const testconfig = defaultConfig - testconfig.useCDN = false - ImportMock.mockOther(configModule, 'config', testconfig) - - assert(!csp.computeDirectives().scriptSrc.includes('https://cdnjs.cloudflare.com')) - assert(!csp.computeDirectives().scriptSrc.includes('https://cdn.mathjax.org')) - assert(!csp.computeDirectives().styleSrc.includes('https://cdnjs.cloudflare.com')) - assert(!csp.computeDirectives().styleSrc.includes('https://fonts.googleapis.com')) - assert(!csp.computeDirectives().fontSrc.includes('https://cdnjs.cloudflare.com')) - assert(!csp.computeDirectives().fontSrc.includes('https://fonts.gstatic.com')) - }) - - it('Disable Google Analytics', function () { - const testconfig = defaultConfig - testconfig.csp.addGoogleAnalytics = false - ImportMock.mockOther(configModule, 'config', testconfig) - - assert(!csp.computeDirectives().scriptSrc.includes('https://www.google-analytics.com')) - }) - - it('Disable Disqus', function () { - const testconfig = defaultConfig - testconfig.csp.addDisqus = false - ImportMock.mockOther(configModule, 'config', testconfig) - - assert(!csp.computeDirectives().scriptSrc.includes('https://disqus.com')) - assert(!csp.computeDirectives().scriptSrc.includes('https://*.disqus.com')) - assert(!csp.computeDirectives().scriptSrc.includes('https://*.disquscdn.com')) - assert(!csp.computeDirectives().styleSrc.includes('https://*.disquscdn.com')) - assert(!csp.computeDirectives().fontSrc.includes('https://*.disquscdn.com')) - }) - - it('Set ReportURI', function () { - const testconfig = defaultConfig - testconfig.csp.reportURI = 'https://example.com/reportURI' - ImportMock.mockOther(configModule, 'config', testconfig) - - assert.strictEqual(csp.computeDirectives().reportUri, 'https://example.com/reportURI') - }) - - it('Set own directives', function () { - const testconfig = defaultConfig - ImportMock.mockOther(configModule, 'config', testconfig) - const unextendedCSP = csp.computeDirectives() - testconfig.csp.directives = { - defaultSrc: ['https://default.example.com'], - scriptSrc: ['https://script.example.com'], - imgSrc: ['https://img.example.com'], - styleSrc: ['https://style.example.com'], - fontSrc: ['https://font.example.com'], - objectSrc: ['https://object.example.com'], - mediaSrc: ['https://media.example.com'], - childSrc: ['https://child.example.com'], - connectSrc: ['https://connect.example.com'] - } - ImportMock.mockOther(configModule, 'config', testconfig) - - const variations = ['default', 'script', 'img', 'style', 'font', 'object', 'media', 'child', 'connect'] - - for (let i = 0; i < variations.length; i++) { - assert.strictEqual(csp.computeDirectives()[variations[i] + 'Src'].toString(), ['https://' + variations[i] + '.example.com'].concat(unextendedCSP[variations[i] + 'Src']).toString()) - } - }) - - /* - * This test reminds us to update the CSP hash for the speaker notes - */ - it('Unchanged hash for reveal.js speaker notes plugin', function () { - const hash = crypto.createHash('sha1') - hash.update(fs.readFileSync(path.join(process.cwd(), '/node_modules/reveal.js/plugin/notes/notes.html'), 'utf8'), 'utf8') - assert.strictEqual(hash.digest('hex'), 'd5d872ae49b5db27f638b152e6e528837204d380') - }) -}) diff --git a/old_src/test/letter-avatars.ts b/old_src/test/letter-avatars.ts deleted file mode 100644 index 0f4f7604b..000000000 --- a/old_src/test/letter-avatars.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-env node, mocha */ - -'use strict' - -import { ImportMock } from 'ts-mock-imports' -import * as configModule from '../lib/config' - -import assert from 'assert' -import * as avatars from '../lib/letter-avatars' - -describe('generateAvatarURL() gravatar enabled', function () { - beforeEach(function () { - // Reset config to make sure we don't influence other tests - const testconfig = { - allowGravatar: true, - serverURL: 'http://localhost:3000', - port: 3000 - } - ImportMock.mockOther(configModule, 'config', testconfig) - }) - - it('should return correct urls', function () { - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'https://cdn.libravatar.org/avatar/d41b5f3508cc3f31865566a47dd0336b?s=400') - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'https://cdn.libravatar.org/avatar/d41b5f3508cc3f31865566a47dd0336b?s=96') - }) - - it('should return correct urls for names with spaces', function () { - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') - }) -}) - -describe('generateAvatarURL() gravatar disabled', function () { - beforeEach(function () { - // Reset config to make sure we don't influence other tests - const testconfig = { - allowGravatar: false, - serverURL: 'http://localhost:3000', - port: 3000 - } - ImportMock.mockOther(configModule, 'config', testconfig) - }) - - it('should return correct urls', function () { - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') - }) - - it('should return correct urls for names with spaces', function () { - assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') - }) -}) diff --git a/old_src/test/user.ts b/old_src/test/user.ts deleted file mode 100644 index dd4f30378..000000000 --- a/old_src/test/user.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-env node, mocha */ - -import { User, sequelize } from '../lib/models' -import assert = require('assert') - -describe('User Sequelize model', function () { - beforeEach(() => { - return sequelize.sync({ force: true }) - }) - - it('stores a password hash on creation and verifies that password', function () { - const userData = { - password: 'test123' - } - const intentionallyInvalidPassword = 'stuff' - - return User.create(userData).then(u => { - return Promise.all([ - u.verifyPassword(userData.password).then(result => assert.strictEqual(result, true)), - u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false)) - ]).catch(e => assert.fail(e)) - }) - }) - - it('can cope with password stored in standard scrypt header format', function () { - const testKey = '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9' - const validPassword = 'test' - const intentionallyInvalidPassword = 'stuff' - - const u = User.build() - u.setDataValue('password', testKey) // this circumvents the setter - which we don't need in this case! - return Promise.all([ - u.verifyPassword(validPassword).then(result => assert.strictEqual(result, true)), - u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false)) - ]).catch(e => assert.fail(e)) - }) - - it('deals with various characters correctly', function () { - const combinations = [ - // ['correct password', 'scrypt syle hash'] - ['test', '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9'], - ['ohai', '736372797074000e00000008000000010efec4e5ce6a5294491f1b1cccc38d3562f84844b9271aef635f8bc338cf4e0e0bac62ebb11379e85894c1f694e038fc39b087b4fdacd1280b50a7382d7ffbfc82f2190bef70d47708d2a94b75126294'], - ['my secret pw', '736372797074000f0000000800000001ffb4cd10a1dfe9e64c1e5416fd6d55b390b6822e78b46fd1f963fe9f317a1e05f9c5fee15e1f618286f4e38b55364ae1e7dc295c9dc33ee0f5712e86afe37e5784ff9c7cf84cf0e631dd11f84f3621e7'], - ['my secret pw', /* different hash! */ '736372797074000f0000000800000001f6083e9593365acd07550f7c72f19973fb7d52c3ef0a78026ff66c48ab14493843c642167b5e6b7f31927e8eeb912bc2639e41955fae15da5099998948cfeacd022f705624931c3b30104e6bb296b805'], - ['i am so extremely long, it\'s not even funny. Wait, you\'re still reading?', '736372797074000f00000008000000012d205f7bb529bb3a8b8bb25f5ab46197c7e9baf1aad64cf5e7b2584c84748cacf5e60631d58d21cb51fa34ea93b517e2fe2eb722931db5a70ff5a1330d821288ee7380c4136369f064b71b191a785a5b'] - ] - const intentionallyInvalidPassword = 'stuff' - - return Promise.all(combinations.map((combination, index) => { - const u = User.build() - u.setDataValue('password', combination[1]) - return Promise.all([ - u.verifyPassword(combination[0]) - .then(result => assert.strictEqual(result, true, `password #${index} "${combination[0]}" should have been verified`)), - u.verifyPassword(intentionallyInvalidPassword) - .then(result => assert.strictEqual(result, false, `password #${index} "${combination[0]}" should NOT have been verified`)) - ]) - })).catch(e => assert.fail(e)) - }) -}) diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..910e0a2bf --- /dev/null +++ b/renovate.json @@ -0,0 +1,15 @@ +{ + "extends": [ + "config:base", + "group:definitelyTyped", + "group:socketio", + "group:linters", + "group:test", + "group:nextjsMonorepo", + ":disableMajorUpdates", + ":gitSignOff" + ], + "labels": [ + "type: maintenance" + ] +}