Repository: requarks/wiki Branch: main Commit: f0f71536ab2a Files: 695 Total size: 2.3 MB Directory structure: gitextract_3rhs9jgy/ ├── .babelrc ├── .devcontainer/ │ └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── auto_assign.yml │ ├── issuecomplete.yml │ └── workflows/ │ ├── build.yml │ ├── helm.yml │ └── packer.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── client/ │ ├── .modernizrrc.js │ ├── client-app.js │ ├── client-setup.js │ ├── components/ │ │ ├── admin/ │ │ │ ├── admin-analytics.vue │ │ │ ├── admin-api-create.vue │ │ │ ├── admin-api.vue │ │ │ ├── admin-auth.vue │ │ │ ├── admin-comments.vue │ │ │ ├── admin-contribute.vue │ │ │ ├── admin-dashboard.vue │ │ │ ├── admin-dev-flags.vue │ │ │ ├── admin-editor.vue │ │ │ ├── admin-extensions.vue │ │ │ ├── admin-general.vue │ │ │ ├── admin-groups-edit-permissions.vue │ │ │ ├── admin-groups-edit-rules.vue │ │ │ ├── admin-groups-edit-users.vue │ │ │ ├── admin-groups-edit.vue │ │ │ ├── admin-groups.vue │ │ │ ├── admin-locale.vue │ │ │ ├── admin-logging-console.vue │ │ │ ├── admin-logging.vue │ │ │ ├── admin-mail.vue │ │ │ ├── admin-navigation.vue │ │ │ ├── admin-pages-edit.vue │ │ │ ├── admin-pages-visualize.vue │ │ │ ├── admin-pages.vue │ │ │ ├── admin-rendering.vue │ │ │ ├── admin-search.vue │ │ │ ├── admin-security.vue │ │ │ ├── admin-ssl.vue │ │ │ ├── admin-stats.vue │ │ │ ├── admin-storage.vue │ │ │ ├── admin-system.vue │ │ │ ├── admin-tags.vue │ │ │ ├── admin-theme.vue │ │ │ ├── admin-users-create.vue │ │ │ ├── admin-users-edit.vue │ │ │ ├── admin-users.vue │ │ │ ├── admin-utilities-auth.vue │ │ │ ├── admin-utilities-cache.vue │ │ │ ├── admin-utilities-content.vue │ │ │ ├── admin-utilities-export.vue │ │ │ ├── admin-utilities-importv1.vue │ │ │ ├── admin-utilities-telemetry.vue │ │ │ ├── admin-utilities.vue │ │ │ └── admin-webhooks.vue │ │ ├── admin.vue │ │ ├── comments.vue │ │ ├── common/ │ │ │ ├── duration-picker.vue │ │ │ ├── loader.vue │ │ │ ├── nav-header.vue │ │ │ ├── notify.vue │ │ │ ├── page-convert.vue │ │ │ ├── page-delete.vue │ │ │ ├── page-selector.vue │ │ │ ├── password-strength.vue │ │ │ ├── search-results.vue │ │ │ ├── social-sharing.vue │ │ │ ├── user-search.vue │ │ │ ├── v-card-chin.vue │ │ │ └── v-card-info.vue │ │ ├── editor/ │ │ │ ├── api/ │ │ │ │ └── server-selector.vue │ │ │ ├── ckeditor/ │ │ │ │ └── conflict.vue │ │ │ ├── common/ │ │ │ │ ├── cmFold.js │ │ │ │ └── katex.js │ │ │ ├── editor-api.vue │ │ │ ├── editor-asciidoc.vue │ │ │ ├── editor-ckeditor.vue │ │ │ ├── editor-code.vue │ │ │ ├── editor-markdown.vue │ │ │ ├── editor-modal-blocks.vue │ │ │ ├── editor-modal-conflict.vue │ │ │ ├── editor-modal-drawio.vue │ │ │ ├── editor-modal-editorselect.vue │ │ │ ├── editor-modal-media.vue │ │ │ ├── editor-modal-properties.vue │ │ │ ├── editor-modal-unsaved.vue │ │ │ ├── editor-redirect.vue │ │ │ └── markdown/ │ │ │ ├── help.vue │ │ │ ├── plantuml.js │ │ │ └── tabset.js │ │ ├── editor.vue │ │ ├── history.vue │ │ ├── login.vue │ │ ├── new-page.vue │ │ ├── not-found.vue │ │ ├── profile/ │ │ │ ├── comments.vue │ │ │ ├── pages.vue │ │ │ └── profile.vue │ │ ├── profile.vue │ │ ├── register.vue │ │ ├── setup.vue │ │ ├── source.vue │ │ ├── tags.vue │ │ ├── unauthorized.vue │ │ └── welcome.vue │ ├── graph/ │ │ ├── admin/ │ │ │ ├── analytics/ │ │ │ │ ├── analytics-mutation-save-providers.gql │ │ │ │ └── analytics-query-providers.gql │ │ │ ├── auth/ │ │ │ │ ├── auth-query-groups.gql │ │ │ │ ├── auth-query-host.gql │ │ │ │ └── auth-query-strategies.gql │ │ │ ├── contribute/ │ │ │ │ └── contribute-query-contributors.gql │ │ │ ├── dashboard/ │ │ │ │ └── dashboard-query-stats.gql │ │ │ ├── dev/ │ │ │ │ ├── dev-mutation-save-flags.gql │ │ │ │ └── dev-query-flags.gql │ │ │ ├── groups/ │ │ │ │ ├── groups-mutation-assign.gql │ │ │ │ ├── groups-mutation-create.gql │ │ │ │ ├── groups-mutation-unassign.gql │ │ │ │ └── groups-query-list.gql │ │ │ ├── locale/ │ │ │ │ ├── locale-mutation-download.gql │ │ │ │ ├── locale-mutation-save.gql │ │ │ │ └── locale-query-list.gql │ │ │ ├── logging/ │ │ │ │ ├── logging-mutation-save-loggers.gql │ │ │ │ ├── logging-query-loggers.gql │ │ │ │ └── logging-subscription-livetrail.gql │ │ │ ├── mail/ │ │ │ │ ├── mail-mutation-save-config.gql │ │ │ │ ├── mail-mutation-sendtest.gql │ │ │ │ └── mail-query-config.gql │ │ │ ├── pages/ │ │ │ │ ├── pages-query-list.gql │ │ │ │ └── pages-query-single.gql │ │ │ ├── rendering/ │ │ │ │ ├── rendering-mutation-save-renderers.gql │ │ │ │ └── rendering-query-renderers.gql │ │ │ ├── search/ │ │ │ │ ├── search-mutation-rebuild-index.gql │ │ │ │ ├── search-mutation-save-engines.gql │ │ │ │ └── search-query-engines.gql │ │ │ ├── storage/ │ │ │ │ ├── storage-mutation-executeaction.gql │ │ │ │ ├── storage-mutation-save-targets.gql │ │ │ │ ├── storage-query-status.gql │ │ │ │ └── storage-query-targets.gql │ │ │ ├── system/ │ │ │ │ ├── system-mutation-upgrade.gql │ │ │ │ └── system-query-info.gql │ │ │ ├── theme/ │ │ │ │ ├── theme-mutation-save.gql │ │ │ │ └── theme-query-config.gql │ │ │ ├── users/ │ │ │ │ ├── users-mutation-create.gql │ │ │ │ └── users-query-groups.gql │ │ │ └── utilities/ │ │ │ ├── utilities-mutation-auth-regencerts.gql │ │ │ ├── utilities-mutation-auth-resetguest.gql │ │ │ ├── utilities-mutation-cache-flushcache.gql │ │ │ ├── utilities-mutation-cache-flushuploads.gql │ │ │ ├── utilities-mutation-content-migratelocale.gql │ │ │ ├── utilities-mutation-content-rebuildtree.gql │ │ │ ├── utilities-mutation-importv1-users.gql │ │ │ ├── utilities-mutation-telemetry-resetid.gql │ │ │ ├── utilities-mutation-telemetry-set.gql │ │ │ └── utilities-query-telemetry.gql │ │ ├── common/ │ │ │ ├── common-localization-query-translations.gql │ │ │ ├── common-pages-mutation-delete.gql │ │ │ ├── common-pages-mutation-move.gql │ │ │ ├── common-pages-query-list.gql │ │ │ ├── common-pages-query-search.gql │ │ │ ├── common-pages-query-tags.gql │ │ │ └── common-pages-query-tree.gql │ │ ├── editor/ │ │ │ ├── editor-media-mutation-asset-delete.gql │ │ │ ├── editor-media-mutation-asset-rename.gql │ │ │ ├── editor-media-mutation-folder-create.gql │ │ │ ├── editor-media-query-folder-list.gql │ │ │ └── editor-media-query-list.gql │ │ ├── login/ │ │ │ ├── login-mutation-changepassword.gql │ │ │ ├── login-mutation-login.gql │ │ │ ├── login-mutation-tfa.gql │ │ │ └── login-query-strategies.gql │ │ └── register/ │ │ └── register-mutation-create.gql │ ├── helpers/ │ │ ├── compatibility.js │ │ └── index.js │ ├── index-app.js │ ├── index-legacy.js │ ├── index-setup.js │ ├── libs/ │ │ ├── animate/ │ │ │ └── animate.scss │ │ ├── codemirror-merge/ │ │ │ └── diff-match-patch.js │ │ ├── markdown-it-underline/ │ │ │ └── index.js │ │ ├── modernizr/ │ │ │ └── modernizr.js │ │ ├── prism/ │ │ │ ├── prism.css │ │ │ └── prism.js │ │ └── twemoji/ │ │ └── twemoji-awesome.scss │ ├── modules/ │ │ ├── boot.js │ │ └── localization.js │ ├── polyfills/ │ │ └── array-from.js │ ├── scss/ │ │ ├── app.scss │ │ ├── base/ │ │ │ ├── animation.scss │ │ │ ├── base.scss │ │ │ ├── icons.scss │ │ │ ├── material.scss │ │ │ └── mixins.scss │ │ ├── components/ │ │ │ ├── codemirror.scss │ │ │ ├── katex.scss │ │ │ ├── v-btn.scss │ │ │ ├── v-data-table.scss │ │ │ ├── v-dialog.scss │ │ │ ├── v-form.scss │ │ │ └── v-tabs.scss │ │ ├── fonts/ │ │ │ ├── arabic.scss │ │ │ └── default.scss │ │ ├── global.scss │ │ ├── layout/ │ │ │ └── _rtl.scss │ │ ├── legacy.scss │ │ └── pages/ │ │ ├── _error.scss │ │ ├── _new.scss │ │ ├── _notfound.scss │ │ ├── _unauthorized.scss │ │ └── _welcome.scss │ ├── static/ │ │ ├── browserconfig.xml │ │ ├── favicons/ │ │ │ └── browserconfig.xml │ │ ├── manifest.json │ │ └── svg/ │ │ └── twemoji.asar │ ├── store/ │ │ ├── admin.js │ │ ├── editor.js │ │ ├── index.js │ │ ├── page.js │ │ ├── site.js │ │ └── user.js │ └── themes/ │ └── default/ │ ├── components/ │ │ ├── nav-footer.vue │ │ ├── nav-sidebar.vue │ │ ├── page.vue │ │ └── tabset.vue │ ├── js/ │ │ └── app.js │ ├── scss/ │ │ └── app.scss │ └── theme.yml ├── config.sample.yml ├── cypress.json ├── dev/ │ ├── build/ │ │ ├── Dockerfile │ │ └── config.yml │ ├── build-arm/ │ │ └── Dockerfile │ ├── containers/ │ │ ├── Dockerfile │ │ ├── config.yml │ │ └── docker-compose.yml │ ├── cypress/ │ │ ├── ci-setup.sh │ │ ├── integration/ │ │ │ └── setup.spec.js │ │ ├── plugins/ │ │ │ └── index.js │ │ └── support/ │ │ ├── commands.js │ │ └── index.js │ ├── examples/ │ │ └── docker-compose.yml │ ├── helm/ │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates/ │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── ingress.yaml │ │ │ ├── postgresql-pvc.yaml │ │ │ ├── postgresql-secret.yaml │ │ │ ├── postgresql-service.yaml │ │ │ ├── postgresql-statefulset.yaml │ │ │ ├── service.yaml │ │ │ ├── serviceaccount.yaml │ │ │ └── tests/ │ │ │ └── test-connection.yaml │ │ └── values.yaml │ ├── index.js │ ├── installer/ │ │ ├── main.go │ │ └── syscheck.go │ ├── openshift/ │ │ └── Dockerfile │ ├── packer/ │ │ ├── digitalocean.json │ │ └── scripts/ │ │ ├── 001-onboot.sh │ │ ├── 010-docker.sh │ │ ├── 011-ufw-docker.sh │ │ ├── 020-force-ssh-logout.sh │ │ ├── 099-one-click │ │ ├── 900-cleanup.sh │ │ └── 999-img-check.sh │ ├── search-engines/ │ │ └── solr/ │ │ └── solrconfig.xml │ ├── templates/ │ │ ├── legacy.pug │ │ ├── master.pug │ │ └── setup.pug │ └── webpack/ │ ├── webpack.dev.js │ └── webpack.prod.js ├── package.json ├── patches/ │ └── extract-files+9.0.0.patch └── server/ ├── app/ │ ├── content/ │ │ └── create.md │ ├── data.yml │ └── regex.js ├── controllers/ │ ├── auth.js │ ├── common.js │ ├── ssl.js │ └── upload.js ├── core/ │ ├── asar.js │ ├── auth.js │ ├── cache.js │ ├── config.js │ ├── db.js │ ├── extensions.js │ ├── kernel.js │ ├── letsencrypt.js │ ├── localization.js │ ├── logger.js │ ├── mail.js │ ├── scheduler.js │ ├── servers.js │ ├── sideloader.js │ ├── system.js │ ├── telemetry.js │ └── worker.js ├── db/ │ ├── beta/ │ │ ├── index.js │ │ ├── migrations/ │ │ │ ├── 2.0.0-beta.1.js │ │ │ ├── 2.0.0-beta.11.js │ │ │ ├── 2.0.0-beta.127.js │ │ │ ├── 2.0.0-beta.148.js │ │ │ ├── 2.0.0-beta.205.js │ │ │ ├── 2.0.0-beta.217.js │ │ │ ├── 2.0.0-beta.242.js │ │ │ ├── 2.0.0-beta.293.js │ │ │ ├── 2.0.0-beta.38.js │ │ │ ├── 2.0.0-beta.99.js │ │ │ ├── 2.0.0-rc.2.js │ │ │ └── 2.0.0-rc.29.js │ │ └── migrations-sqlite/ │ │ ├── 2.0.0-beta.1.js │ │ ├── 2.0.0-beta.11.js │ │ ├── 2.0.0-beta.127.js │ │ ├── 2.0.0-beta.205.js │ │ ├── 2.0.0-beta.217.js │ │ ├── 2.0.0-beta.242.js │ │ ├── 2.0.0-beta.293.js │ │ ├── 2.0.0-beta.38.js │ │ ├── 2.0.0-beta.99.js │ │ └── 2.0.0-rc.2.js │ ├── migrations/ │ │ ├── 2.0.0.js │ │ ├── 2.1.85.js │ │ ├── 2.2.17.js │ │ ├── 2.2.3.js │ │ ├── 2.3.10.js │ │ ├── 2.3.23.js │ │ ├── 2.4.13.js │ │ ├── 2.4.14.js │ │ ├── 2.4.36.js │ │ ├── 2.4.61.js │ │ ├── 2.5.1.js │ │ ├── 2.5.108.js │ │ ├── 2.5.118.js │ │ ├── 2.5.12.js │ │ ├── 2.5.122.js │ │ └── 2.5.128.js │ ├── migrations-sqlite/ │ │ ├── 2.0.0.js │ │ ├── 2.2.17.js │ │ ├── 2.2.3.js │ │ ├── 2.3.10.js │ │ ├── 2.3.14.js │ │ ├── 2.3.23.js │ │ ├── 2.4.13.js │ │ ├── 2.4.36.js │ │ ├── 2.4.61.js │ │ ├── 2.5.1.js │ │ ├── 2.5.108.js │ │ ├── 2.5.118.js │ │ ├── 2.5.12.js │ │ ├── 2.5.122.js │ │ └── 2.5.128.js │ └── migrator-source.js ├── graph/ │ ├── directives/ │ │ ├── auth.js │ │ └── rate-limit.js │ ├── index.js │ ├── resolvers/ │ │ ├── analytics.js │ │ ├── asset.js │ │ ├── authentication.js │ │ ├── comment.js │ │ ├── contribute.js │ │ ├── folder.js │ │ ├── group.js │ │ ├── localization.js │ │ ├── logging.js │ │ ├── mail.js │ │ ├── navigation.js │ │ ├── page.js │ │ ├── rendering.js │ │ ├── search.js │ │ ├── site.js │ │ ├── storage.js │ │ ├── system.js │ │ ├── tag.js │ │ ├── theming.js │ │ └── user.js │ ├── scalars/ │ │ └── date.js │ └── schemas/ │ ├── analytics.graphql │ ├── asset.graphql │ ├── authentication.graphql │ ├── comment.graphql │ ├── common.graphql │ ├── contribute.graphql │ ├── group.graphql │ ├── localization.graphql │ ├── logging.graphql │ ├── mail.graphql │ ├── navigation.graphql │ ├── page.graphql │ ├── rendering.graphql │ ├── scalars.graphql │ ├── search.graphql │ ├── site.graphql │ ├── storage.graphql │ ├── system.graphql │ ├── theming.graphql │ └── user.graphql ├── helpers/ │ ├── asset.js │ ├── brute-knex.js │ ├── common.js │ ├── config.js │ ├── error.js │ ├── graph.js │ ├── page.js │ └── security.js ├── index.js ├── jobs/ │ ├── fetch-graph-locale.js │ ├── purge-uploads.js │ ├── rebuild-tree.js │ ├── render-page.js │ ├── sanitize-svg.js │ ├── sync-graph-locales.js │ ├── sync-graph-updates.js │ └── sync-storage.js ├── locales/ │ └── README.md ├── master.js ├── middlewares/ │ ├── security.js │ └── seo.js ├── models/ │ ├── analytics.js │ ├── apiKeys.js │ ├── assetFolders.js │ ├── assets.js │ ├── authentication.js │ ├── commentProviders.js │ ├── comments.js │ ├── editors.js │ ├── groups.js │ ├── locales.js │ ├── loggers.js │ ├── navigation.js │ ├── pageHistory.js │ ├── pageLinks.js │ ├── pages.js │ ├── renderers.js │ ├── searchEngines.js │ ├── settings.js │ ├── storage.js │ ├── tags.js │ ├── userKeys.js │ └── users.js ├── modules/ │ ├── analytics/ │ │ ├── azureinsights/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── baidutongji/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── countly/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── elasticapm/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── fathom/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── fullstory/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── google/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── gtm/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── hotjar/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── matomo/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── newrelic/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── plausible/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── statcounter/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── umami/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── umami2/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ └── yandex/ │ │ ├── code.yml │ │ └── definition.yml │ ├── authentication/ │ │ ├── auth0/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── azure/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── cas/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── discord/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── dropbox/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── facebook/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── firebase/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── github/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── gitlab/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── google/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── keycloak/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── ldap/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── local/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── microsoft/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── oauth2/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── oidc/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── okta/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── rocketchat/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── saml/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ ├── slack/ │ │ │ ├── authentication.js │ │ │ └── definition.yml │ │ └── twitch/ │ │ ├── authentication.js │ │ └── definition.yml │ ├── comments/ │ │ ├── artalk/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── commento/ │ │ │ ├── code.yml │ │ │ └── definition.yml │ │ ├── default/ │ │ │ ├── comment.js │ │ │ └── definition.yml │ │ └── disqus/ │ │ ├── code.yml │ │ └── definition.yml │ ├── editor/ │ │ ├── api/ │ │ │ └── definition.yml │ │ ├── asciidoc/ │ │ │ └── definition.yml │ │ ├── ckeditor/ │ │ │ └── definition.yml │ │ ├── code/ │ │ │ └── definition.yml │ │ ├── markdown/ │ │ │ └── definition.yml │ │ ├── redirect/ │ │ │ └── definition.yml │ │ └── wysiwyg/ │ │ └── definition.yml │ ├── extensions/ │ │ ├── git/ │ │ │ └── ext.js │ │ ├── pandoc/ │ │ │ └── ext.js │ │ ├── puppeteer/ │ │ │ └── ext.js │ │ └── sharp/ │ │ └── ext.js │ ├── logging/ │ │ ├── airbrake/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── bugsnag/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── disk/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── eventlog/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── loggly/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── logstash/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── newrelic/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── papertrail/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── raygun/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── rollbar/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ ├── sentry/ │ │ │ ├── definition.yml │ │ │ └── logger.js │ │ └── syslog/ │ │ ├── definition.yml │ │ └── logger.js │ ├── rendering/ │ │ ├── asciidoc-core/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-asciinema/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-blockquotes/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-codehighlighter/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-core/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-diagram/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-image-prefetch/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-mediaplayers/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-mermaid/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-security/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-tabset/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── html-twemoji/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-abbr/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-core/ │ │ │ ├── definition.yml │ │ │ ├── renderer.js │ │ │ └── underline.js │ │ ├── markdown-emoji/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-expandtabs/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-footnotes/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-imsize/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-katex/ │ │ │ ├── definition.yml │ │ │ ├── mhchem.js │ │ │ └── renderer.js │ │ ├── markdown-kroki/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-mathjax/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-multi-table/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-pivot-table/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-plantuml/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-supsub/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ ├── markdown-tasklists/ │ │ │ ├── definition.yml │ │ │ └── renderer.js │ │ └── openapi-core/ │ │ ├── definition.yml │ │ └── renderer.js │ ├── search/ │ │ ├── algolia/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── aws/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── azure/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── db/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── elasticsearch/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── manticore/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── postgres/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ ├── solr/ │ │ │ ├── definition.yml │ │ │ └── engine.js │ │ └── sphinx/ │ │ ├── definition.yml │ │ └── engine.js │ └── storage/ │ ├── azure/ │ │ ├── definition.yml │ │ └── storage.js │ ├── box/ │ │ ├── definition.yml │ │ └── storage.js │ ├── digitalocean/ │ │ ├── definition.yml │ │ └── storage.js │ ├── disk/ │ │ ├── common.js │ │ ├── definition.yml │ │ └── storage.js │ ├── dropbox/ │ │ ├── definition.yml │ │ └── storage.js │ ├── gdrive/ │ │ ├── definition.yml │ │ └── storage.js │ ├── git/ │ │ ├── definition.yml │ │ └── storage.js │ ├── onedrive/ │ │ ├── definition.yml │ │ └── storage.js │ ├── s3/ │ │ ├── common.js │ │ ├── definition.yml │ │ └── storage.js │ ├── s3generic/ │ │ ├── definition.yml │ │ └── storage.js │ └── sftp/ │ ├── definition.yml │ └── storage.js ├── setup.js ├── templates/ │ ├── account-reset-pwd.html │ ├── account-verify.html │ └── test.html ├── test/ │ └── helpers/ │ └── page.test.js ├── themes/ │ └── default/ │ └── theme.yml └── views/ ├── admin.pug ├── editor.pug ├── error.pug ├── history.pug ├── legacy/ │ ├── login.pug │ └── page.pug ├── login.pug ├── new.pug ├── notfound.pug ├── page.pug ├── profile.pug ├── register.pug ├── source.pug ├── tags.pug ├── unauthorized.pug └── welcome.pug ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "comments": true, "plugins": [ "lodash", "graphql-tag", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-json-strings", [ "@babel/plugin-proposal-decorators", { "legacy": true } ], "@babel/plugin-proposal-function-sent", "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-throw-expressions", [ "prismjs", { "languages": ["clike", "markup"], "plugins": ["line-numbers", "autoloader", "normalize-whitespace", "copy-to-clipboard", "toolbar"], "theme": "twilight", "css": true } ] ], "presets": [ [ "@babel/preset-env", { "useBuiltIns": "entry", "corejs": 3, "debug": false } ] ] } ================================================ FILE: .devcontainer/devcontainer.json ================================================ // How to get remote container development working with VSCode: // 1. Install "Remote Development" extension pack (ms-vscode-remote.vscode-remote-extensionpack) // 2. Select "Remote Containers - Reopen in container" { "name": "Wiki.js", "dockerComposeFile": [ "../dev/containers/docker-compose.yml" ], "forwardPorts": [3000, 3001], "service": "wiki", "workspaceFolder": "/wiki", "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, "extensions": [ "EditorConfig.editorconfig", "dbaeumer.vscode-eslint", "christian-kohler.path-intellisense", "mrmlnc.vscode-puglint", "octref.vetur", "dzannotti.vscode-babel-coloring", "wayou.vscode-todo-highlight", "visualstudioexptteam.vscodeintellicode", "lukas-tr.materialdesignicons-intellisense", "codezombiech.gitignore", "kumar-harsh.graphql-for-vscode", "mrmlnc.vscode-duplicate", "oderwat.indent-rainbow", "christian-kohler.npm-intellisense" ], "postCreateCommand": ["yarn", "install"] } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true end_of_line = lf insert_final_newline = true [*.{jade,pug,md}] trim_trailing_whitespace = false [Makefile] indent_style = tab indent_size = 4 ================================================ FILE: .eslintignore ================================================ **/node_modules/** **/*.min.js assets/** client/libs/** coverage/** repo/** data/** logs/** ================================================ FILE: .eslintrc.yml ================================================ extends: - requarks - plugin:vue/strongly-recommended - plugin:cypress/recommended env: node: true jest: true parserOptions: parser: babel-eslint ecmaVersion: 2017 allowImportExportEverywhere: true globals: document: false navigator: false window: false ================================================ FILE: .gitattributes ================================================ # Common settings that generally should always be used with your language specific settings # Auto detect text files and perform LF normalization # https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ * text=auto # # The above will handle all files NOT found below # # Documents *.bibtex text diff=bibtex *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain *.md text *.tex text diff=tex *.adoc text *.textile text *.mustache text *.csv text *.tab text *.tsv text *.txt text *.sql text # Graphics *.png binary *.jpg binary *.jpeg binary *.gif binary *.tif binary *.tiff binary *.ico binary # SVG treated as an asset (binary) by default. *.svg text # If you want to treat it as binary, # use the following line instead. # *.svg binary *.eps binary # Scripts *.bash text eol=lf *.sh text eol=lf # These are explicitly windows files and should use crlf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf # Serialisation *.json text *.toml text *.xml text *.yaml text *.yml text # Archives *.7z binary *.gz binary *.tar binary *.zip binary # # Exclude files from exporting # .gitattributes export-ignore .gitignore export-ignore # Auto detect text files and perform LF normalization # https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ * text=auto *.cs text diff=csharp # Treat all Go files in this repo as binary, with no git magic updating # line endings. Windows users contributing to Go will need to use a # modern version of git and editors capable of LF line endings. *.go -text diff=golang ## GITATTRIBUTES FOR WEB PROJECTS # # These settings are for any web project. # # Details per file setting: # text These files should be normalized (i.e. convert CRLF to LF). # binary These files are binary and should be left untouched. # # Note that binary is a macro for -text -diff. ###################################################################### # Auto detect ## Handle line endings automatically for files detected as ## text and leave all files detected as binary untouched. ## This will handle all files NOT defined below. * text=auto # Source code *.bash text eol=lf *.bat text eol=crlf *.cmd text eol=crlf *.coffee text *.css text *.htm text diff=html *.html text diff=html *.inc text *.ini text *.js text *.json text *.jsx text *.less text *.ls text *.map text -diff *.od text *.onlydata text *.php text diff=php *.pl text *.ps1 text eol=crlf *.py text diff=python *.rb text diff=ruby *.sass text *.scm text *.scss text diff=css *.sh text eol=lf *.sql text *.styl text *.tag text *.ts text *.tsx text *.xml text *.xhtml text diff=html # Docker *.dockerignore text Dockerfile text # Documentation *.ipynb text *.markdown text *.md text *.mdwn text *.mdown text *.mkd text *.mkdn text *.mdtxt text *.mdtext text *.txt text AUTHORS text CHANGELOG text CHANGES text CONTRIBUTING text COPYING text copyright text *COPYRIGHT* text INSTALL text license text LICENSE text NEWS text readme text *README* text TODO text # Templates *.dot text *.ejs text *.haml text *.handlebars text *.hbs text *.hbt text *.jade text *.latte text *.mustache text *.njk text *.phtml text *.tmpl text *.tpl text *.twig text *.vue text # Linters .csslintrc text .eslintrc text .htmlhintrc text .jscsrc text .jshintrc text .jshintignore text .stylelintrc text # Configs *.bowerrc text *.cnf text *.conf text *.config text .babelrc text .browserslistrc text .editorconfig text .env text .gitattributes text .gitconfig text .htaccess text *.lock text -diff package-lock.json text -diff *.npmignore text *.yaml text *.yml text browserslist text Makefile text makefile text # Heroku Procfile text .slugignore text # Graphics *.ai binary *.bmp binary *.eps binary *.gif binary *.gifv binary *.ico binary *.jng binary *.jp2 binary *.jpg binary *.jpeg binary *.jpx binary *.jxr binary *.pdf binary *.png binary *.psb binary *.psd binary # SVG treated as an asset (binary) by default. *.svg text # If you want to treat it as binary, # use the following line instead. # *.svg binary *.svgz binary *.tif binary *.tiff binary *.wbmp binary *.webp binary # Audio *.kar binary *.m4a binary *.mid binary *.midi binary *.mp3 binary *.ogg binary *.ra binary # Video *.3gpp binary *.3gp binary *.as binary *.asf binary *.asx binary *.fla binary *.flv binary *.m4v binary *.mng binary *.mov binary *.mp4 binary *.mpeg binary *.mpg binary *.ogv binary *.swc binary *.swf binary *.webm binary # Archives *.7z binary *.gz binary *.jar binary *.rar binary *.tar binary *.zip binary # Fonts *.ttf binary *.eot binary *.otf binary *.woff binary *.woff2 binary # Executables *.exe binary *.pyc binary ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abuse@requarks.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contribute ## Introduction First, thank you for considering contributing to Wiki.js! It's people like you that make the open source community such a great community! 😊 We welcome any type of contribution, not only code. You can help with - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) - **Marketing**: writing blog posts, howto's, printing stickers, ... - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... - **Code**: take a look at the [open issues](https://github.com/Requarks/wiki/issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wikijs). ## Your First Contribution Working on your first Pull Request? You can learn how from this *free* course, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). ## Submitting code Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. ## Code review process The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? ## Requesting new features / enhancements Use the feature request board to submit new ideas and vote on which ideas should be integrated first. :triangular_flag_on_post: [https://js.wiki/feedback/](https://js.wiki/feedback/) *Do not use GitHub issues to submit new feature ideas, as it will closed and you'll be asked to use the feature request board above. GitHub Issues are limited to bugs / issues / help*. ## Financial contributions We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wikijs). Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. ## Questions If you have any questions, create an [issue](https://github.com/Requarks/wiki/issues/new/choose) (protip: do a quick search first to see if someone else didn't ask the same question before!). You can also reach us at . ## Credits ### Contributors Thank you to all the people who have already contributed to Wiki.js! ### Backers Thank you to all our backers! [[Become a backer](https://opencollective.com/wikijs#backer)] ### Sponsors Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/wikijs#sponsor)) ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [NGPixel] patreon: requarks open_collective: wikijs ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel custom: # Replace with a single custom sponsorship URL ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Help / Questions url: https://github.com/Requarks/wiki/discussions/categories/help-questions about: Ask the community for help on using or setting up Wiki.js - name: Errors / Bug Reports url: https://github.com/Requarks/wiki/discussions/categories/error-bug-report about: Create a discussion around the bug / error you're getting. If validated, a proper GitHub issue will be created so that it can be worked on. - name: Request a new feature / improvement url: https://feedback.js.wiki/wiki about: Submit ideas for new features or improvements. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/auto_assign.yml ================================================ # Set to true to add reviewers to pull requests addReviewers: true # Set to true to add assignees to pull requests addAssignees: true # A list of reviewers to be added to pull requests (GitHub user name) reviewers: - NGPixel # A list of keywords to be skipped the process that add reviewers if pull requests include it skipKeywords: - wip # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) numberOfReviewers: 0 ================================================ FILE: .github/issuecomplete.yml ================================================ # The name of the label to apply when an issue does not have all tasks checked labelName: invalid # The color of the label in hex format (without #) labelColor: # The text of the comment to add to the issue in addition to the label commentText: > You haven't provided the required info about your host! (OS, Wiki.js version, Database engine) # Whether or not to ensure all checkboxes are checked checkCheckboxes: false # Keywords to look for in the body of the issue keywords: - Wiki.js version - Database engine ================================================ FILE: .github/workflows/build.yml ================================================ name: Build + Publish on: push: branches: - main tags: - 'v*' env: BASE_DEV_VERSION: 2.5.0 jobs: build: name: Build runs-on: ubuntu-latest permissions: packages: write steps: - uses: actions/checkout@v4 - name: Set Build Variables run: | if [[ "$GITHUB_REF" =~ ^refs/tags/v* ]]; then echo "Using TAG mode: $GITHUB_REF_NAME" echo "REL_VERSION=$GITHUB_REF_NAME" >> $GITHUB_ENV echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV else echo "Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" echo "REL_VERSION=v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_ENV echo "REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_ENV fi - name: Disable DEV Flag + Set Version run: | sudo apt-get install jq -y mv package.json pkg-temp.json jq --arg vs "$REL_VERSION_STRICT" -r '. + {dev:false, version:$vs}' pkg-temp.json > package.json rm pkg-temp.json cat package.json - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker images uses: docker/build-push-action@v5 with: context: . file: dev/build/Dockerfile push: true tags: | requarks/wiki:canary requarks/wiki:canary-${{ env.REL_VERSION_STRICT }} ghcr.io/requarks/wiki:canary ghcr.io/requarks/wiki:canary-${{ env.REL_VERSION_STRICT }} - name: Extract compiled files run: | mkdir -p _dist docker create --name wiki ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT docker cp wiki:/wiki _dist docker rm wiki rm _dist/wiki/config.yml cp ./config.sample.yml _dist/wiki/config.sample.yml find _dist/wiki/ -printf "%P\n" | tar -czf wiki-js.tar.gz --no-recursion -C _dist/wiki/ -T - - name: Upload a Build Artifact uses: actions/upload-artifact@v4 with: name: drop path: wiki-js.tar.gz cypress: name: Run Cypress Tests runs-on: ubuntu-latest needs: [build] strategy: matrix: dbtype: [postgres, mysql, mariadb, sqlite] steps: - uses: actions/checkout@v4 - name: Set Test Variables run: | if [[ "$GITHUB_REF" =~ ^refs/tags/v* ]]; then echo "Using TAG mode: $GITHUB_REF_NAME" echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV else echo "Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" echo "REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_ENV fi - name: Run Tests env: MATRIXENV: ${{ matrix.dbtype }} CYPRESS_KEY: ${{ secrets.CYPRESS_KEY }} run: | chmod u+x dev/cypress/ci-setup.sh dev/cypress/ci-setup.sh docker run --name cypress --ipc=host --shm-size 1G -v $GITHUB_WORKSPACE:/e2e -w /e2e cypress/included:4.9.0 --record --key "$CYPRESS_KEY" --headless --group "$MATRIXENV" --ci-build-id "$REL_VERSION_STRICT-run$GITHUB_RUN_NUMBER.$GITHUB_RUN_ATTEMPT" --tag "$REL_VERSION_STRICT" --config baseUrl=http://172.17.0.1:3000 arm: name: ARM Build runs-on: ubuntu-latest needs: [cypress] permissions: packages: write strategy: matrix: include: - platform: linux/arm64 docker: arm64 # - platform: linux/arm/v7 # docker: armv7 steps: - uses: actions/checkout@v4 - name: Set Version Variables run: | if [[ "$GITHUB_REF" =~ ^refs/tags/v* ]]; then echo "Using TAG mode: $GITHUB_REF_NAME" echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV else echo "Using BRANCH mode: v$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" echo "REL_VERSION_STRICT=$BASE_DEV_VERSION-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_ENV fi - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Download a Build Artifact uses: actions/download-artifact@v4 with: name: drop path: drop - name: Extract Build run: | mkdir -p build tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules - name: Build and push Docker images uses: docker/build-push-action@v5 with: context: . file: dev/build-arm/Dockerfile platforms: ${{ matrix.platform }} provenance: false push: true tags: | requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }} ghcr.io/requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }} windows: name: Windows Build runs-on: windows-latest needs: [cypress] steps: - name: Setup Node.js environment uses: actions/setup-node@v4 with: node-version: 20.x - name: Download a Build Artifact uses: actions/download-artifact@v4 with: name: drop path: drop - name: Extract Build run: | mkdir -p win tar -xzf $env:GITHUB_WORKSPACE\drop\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\win Copy-Item win\node_modules\extract-files\package.json patch-extractfile.json -Force Remove-Item -Path win\node_modules -Force -Recurse - name: Install Dependencies run: | yarn --production --frozen-lockfile --non-interactive yarn patch-package working-directory: win - name: Fix patched packages run: | Copy-Item patch-extractfile.json win\node_modules\extract-files\package.json -Force - name: Create Bundle run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win . - name: Upload a Build Artifact uses: actions/upload-artifact@v4 with: name: drop-win path: wiki-js-windows.tar.gz beta: name: Publish Beta Images runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') needs: [build, arm, windows] permissions: packages: write steps: - name: Set Version Variables run: | echo "Using TAG mode: $GITHUB_REF_NAME" echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and Push Manifests run: | echo "Creating the manifests..." docker manifest create requarks/wiki:beta-$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create ghcr.io/requarks/wiki:beta-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT echo "Pushing the manifests..." docker manifest push -p requarks/wiki:beta-$REL_VERSION_STRICT docker manifest push -p ghcr.io/requarks/wiki:beta-$REL_VERSION_STRICT release: name: Publish Release Images runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') environment: prod needs: [beta] permissions: packages: write contents: write steps: - name: Set Version Variables run: | echo "Using TAG mode: $GITHUB_REF_NAME" echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and Push Manifests run: | echo "Fetching semver tool..." curl -LJO https://static.requarks.io/semver chmod +x semver MAJOR=`./semver get major $REL_VERSION_STRICT` MINOR=`./semver get minor $REL_VERSION_STRICT` MAJORMINOR="$MAJOR.$MINOR" echo "Using major $MAJOR and minor $MINOR..." echo "Creating the manifests..." docker manifest create requarks/wiki:$REL_VERSION_STRICT requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create requarks/wiki:$MAJOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create requarks/wiki:$MAJORMINOR requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create requarks/wiki:latest requarks/wiki:canary-$REL_VERSION_STRICT requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create ghcr.io/requarks/wiki:$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create ghcr.io/requarks/wiki:$MAJOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create ghcr.io/requarks/wiki:$MAJORMINOR ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT docker manifest create ghcr.io/requarks/wiki:latest ghcr.io/requarks/wiki:canary-$REL_VERSION_STRICT ghcr.io/requarks/wiki:canary-arm64-$REL_VERSION_STRICT echo "Pushing the manifests..." docker manifest push -p requarks/wiki:$REL_VERSION_STRICT docker manifest push -p requarks/wiki:$MAJOR docker manifest push -p requarks/wiki:$MAJORMINOR docker manifest push -p requarks/wiki:latest docker manifest push -p ghcr.io/requarks/wiki:$REL_VERSION_STRICT docker manifest push -p ghcr.io/requarks/wiki:$MAJOR docker manifest push -p ghcr.io/requarks/wiki:$MAJORMINOR docker manifest push -p ghcr.io/requarks/wiki:latest - name: Download Linux Build uses: actions/download-artifact@v4 with: name: drop path: drop - name: Download Windows Build uses: actions/download-artifact@v4 with: name: drop-win path: drop-win - name: Generate Changelog id: changelog uses: Requarks/changelog-action@v1 with: token: ${{ github.token }} tag: ${{ github.ref_name }} writeToFile: false - name: Update GitHub Release uses: ncipollo/release-action@v1.12.0 with: allowUpdates: true draft: false makeLatest: true name: ${{ github.ref_name }} body: ${{ steps.changelog.outputs.changes }} token: ${{ github.token }} artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz' # - name: Notify Slack Releases Channel # uses: slackapi/slack-github-action@v1.26.0 # with: # payload: | # { # "text": "Wiki.js ${{ github.ref_name }} has been released." # } # env: # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK - name: Notify Telegram Channel uses: appleboy/telegram-action@v0.1.1 with: to: ${{ secrets.TELEGRAM_TO }} token: ${{ secrets.TELEGRAM_TOKEN }} format: markdown disable_web_page_preview: true message: | Wiki.js *${{ github.ref_name }}* has been released! See [release notes](https://github.com/requarks/wiki/releases) for details. - name: Notify Discord Channel uses: sebastianpopp/discord-action@v2.0 with: webhook: ${{ secrets.DISCORD_WEBHOOK }} message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details. # build-do-image: # name: Build DigitalOcean Image # runs-on: ubuntu-latest # needs: [release] # steps: # - uses: actions/checkout@v4 # - name: Set Version Variables # run: | # echo "Using TAG mode: $GITHUB_REF_NAME" # echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV # - name: Install Packer # run: | # curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - # sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" # sudo apt-get update && sudo apt-get install packer # - name: Build Droplet Image # env: # DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }} # WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }} # working-directory: dev/packer # run: | # packer build digitalocean.json ================================================ FILE: .github/workflows/helm.yml ================================================ name: Helm Chart CI on: # Triggers the workflow on push or pull request events but only for the dev branch push: branches: [ main ] paths: [ dev/helm/** ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: build: name: Update Chart runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v6 - name: Package and Push Chart run: | export CHARTVER=$(yq '.version' dev/helm/Chart.yaml) helm plugin install https://github.com/chartmuseum/helm-push.git helm repo add chartmuseum https://charts.js.wiki helm cm-push --version="$CHARTVER" --username="${{secrets.HELM_REPO_USERNAME}}" --password="${{secrets.HELM_REPO_PASSWORD}}" dev/helm/ chartmuseum helm repo remove chartmuseum ================================================ FILE: .github/workflows/packer.yml ================================================ name: Build DigitalOcean Image on: workflow_dispatch: inputs: version: description: 'App Version' required: true type: string jobs: build-do-image: name: Build DigitalOcean Image runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Install Packer run: | wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt update && sudo apt install packer - name: Build Droplet Image env: DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }} WIKI_APP_VERSION: ${{ github.event.inputs.version }} working-directory: dev/packer run: | packer plugins install github.com/digitalocean/digitalocean packer build digitalocean.json ================================================ FILE: .gitignore ================================================ # Logs logs *.log /logs # Deployment builds dist # Dependency directories node_modules npm/node_modules # NPM / Yarn .npm .node_repl_history npm-debug.log* .yarn # Generated assets /assets server/views/master.pug server/views/legacy/master.pug server/views/setup.pug # Webpack .webpack-cache .fusebox # Config Files /config.yml # Data directories /repo /data /uploads /content /temp *.sqlite # IDE exclude .idea *.sublime-* # Test results test-results/ .scannerwork # Localization Resources /server/locales/**/*.yml ================================================ FILE: .npmrc ================================================ save-exact = true save-prefix = "" ================================================ FILE: .nvmrc ================================================ v24.12.0 ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "EditorConfig.editorconfig", "dbaeumer.vscode-eslint", "christian-kohler.path-intellisense", "mrmlnc.vscode-puglint", "octref.vetur" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible Node.js debug attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach (Inspector Protocol)", "port": 9229, "protocol": "inspector" }, { "type": "node", "request": "launch", "name": "Launch Program", "program": "${workspaceRoot}\\server.js" }, { "type": "node", "request": "attach", "name": "Attach to Port", "address": "localhost", "port": 9222 } ] } ================================================ FILE: .vscode/settings.json ================================================ { "eslint.enable": true, "puglint.enable": true, "editor.formatOnSave": false, "editor.tabSize": 2, "eslint.validate": [ "javascript", "vue" ], "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "i18n-ally.localesPaths": [ "server/locales" ] } ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================
Wiki.js [![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases) [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE) [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/) [![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs) [![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/) [![Chat on Discord](https://img.shields.io/badge/discord-join-8D96F6.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/rcxt9QS2jd) [![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40js.wiki-blue.svg?style=flat&logo=bluesky&logoColor=white)](https://bsky.app/profile/js.wiki) [![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js) [![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/) ##### A modern, lightweight and powerful wiki app built on NodeJS
- **[Official Website](https://js.wiki/)** - **[Documentation](https://docs.requarks.io/)** - [Requirements](https://docs.requarks.io/install/requirements) - [Installation](https://docs.requarks.io/install) - [Demo](https://docs.requarks.io/demo) - [Changelog](https://github.com/requarks/wiki/releases) - [Feature Requests](https://feedback.js.wiki/wiki) - Chat with us on [Discord](https://discord.gg/rcxt9QS2jd) - [Translations](https://docs.requarks.io/dev/translations) *(We need your help!)* - [E2E Testing Results](https://dashboard.cypress.io/projects/r7qxah/runs) - [Special Thanks](#special-thanks) - [Contribute](#contributors) [Follow our Twitter feed](https://twitter.com/requarks) to learn about upcoming updates and new releases!

Donate

Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://js.wiki/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`). [![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship) [![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks) [![Donate on OpenCollective](https://img.shields.io/badge/donate-open%20collective-blue.svg?style=popout&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNTZweCIgaGVpZ2h0PSIyNTZweCIgdmlld0JveD0iMCAwIDI1NiAyNTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxnPjxwYXRoIGQ9Ik0yMDkuNzY1MTQ0LDEyOC4xNDk5NzkgQzIwOS43NjUxNDQsMTQ0LjE2MzMgMjA0Ljg2NDM4MSwxNTkuNDg5ODkgMTk2LjQ5ODc0NywxNzIuNzI1MDcyIEwyMjkuOTQ1Njc1LDIwNi4xNzE5OTkgQzI0Ni42ODIxMDUsMTgzLjg1Njc1OSAyNTUuNzI5MzA3LDE1Ni43MTUxNTIgMjU1LjcyOTMwNywxMjguODIxMTAyIEMyNTUuNzI5MzA3LDk5LjU1Njk5MTcgMjQ1Ljk3NDYwMyw3My4wNzEwMjA3IDIyOS4yNTg5NDQsNTEuNDg1ODEyOCBMMTk2LjQ4MzE0LDg0LjIxNDc5NCBDMjA1LjEyMjU2MSw5Ny4yMjI0NjgzIDIwOS43MzY5MDcsMTEyLjQ4NzgxIDIwOS43NDk1MzcsMTI4LjEwMzE1NiBMMjA5Ljc2NTE0NCwxMjguMTQ5OTc5IFoiIGZpbGw9IiNCOEQzRjQiPjwvcGF0aD48cGF0aCBkPSJNMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEM4Mi4xNDYwODcyLDIxMC4yNjg5NTggNDUuMzg3NTA5NCwxNzMuNTE3MzU4IDQ1LjI5MzAzOTMsMTI4LjE0OTk3OSBDNDUuMzYxNzUwMiw4Mi43NjQzMTM4IDgyLjEyNzg0ODcsNDUuOTg0MjU3IDEyNy41MTM0ODQsNDUuODk4MzE4NiBDMTQ0LjI0NDc1Miw0NS44OTgzMTg2IDE1OS41NzEzNDIsNTAuNzk5MDgxNyAxNzIuMTE5NzkyLDU5LjE2NDcxNTQgTDIwNC44NjQzODEsMjYuMzg4OTExNiBDMTgyLjU0MzY1LDkuNjY2NjUxMjkgMTU1LjQwMzQyOSwwLjYzMDg2MzI5OCAxMjcuNTEzNDg0LDAuNjM2NDk0NDAzIEM1Ny4xMjM1NDM3LDAuNjM2NDk0NDAzIDAsNTcuNzYwMDM4MSAwLDEyOC4xNDk5NzkgQzAsMTk4LjUwODcwNCA1Ny4xMjM1NDM3LDI1NS42NjM0NjMgMTI3LjUxMzQ4NCwyNTUuNjYzNDYzIEMxNTUuNTM3MzUyLDI1NS43NDA4NzYgMTgyLjc3NTk4OSwyNDYuNDA4NTEgMjA0Ljg2NDM4MSwyMjkuMTYxODg0IEwxNzEuNDE3NDU0LDE5NS43MzA1NjQgQzE1OS41NTU3MzQsMjA1LjQ4NTI2OCAxNDQuMjYwMzU5LDIxMC4zNTQ4MTYgMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEwxMjcuNTEzNDg0LDIxMC4zNTQ4MTYgWiIgZmlsbD0iIzdGQURGMiI+PC9wYXRoPjwvZz48L3N2Zz4=)](https://opencollective.com/wikijs) [![Donate via Paypal](https://img.shields.io/badge/donate-paypal-blue.svg?style=popout&logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) [![Donate via Ethereum](https://img.shields.io/badge/donate-ethereum-999.svg?style=popout&logo=ethereum&logoColor=CCC)](https://etherscan.io/address/0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5) [![Donate via Bitcoin](https://img.shields.io/badge/donate-bitcoin-ff9900.svg?style=popout&logo=bitcoin&logoColor=CCC)](https://checkout.opennode.com/p/2553c612-f863-4407-82b3-1a7685268747) [![Buy a T-Shirt](https://img.shields.io/badge/buy-t--shirts-teal.svg?style=popout&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4Igp3aWR0aD0iMjQiIGhlaWdodD0iMjQiCnZpZXdCb3g9IjAgMCAxOTIgMTkyIgpzdHlsZT0iIGZpbGw6IzAwMDAwMDsiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIGZvbnQtZmFtaWx5PSJub25lIiBmb250LXdlaWdodD0ibm9uZSIgZm9udC1zaXplPSJub25lIiB0ZXh0LWFuY2hvcj0ibm9uZSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0wLDE5MnYtMTkyaDE5MnYxOTJ6IiBmaWxsPSJub25lIj48L3BhdGg+PGcgZmlsbD0iIzFhYmM5YyI+PGcgaWQ9InN1cmZhY2UxIj48cGF0aCBkPSJNOTYsMGMtMTUuMjE4NzUsMCAtMjQuNjg3NSwzLjY1NjI1IC0yNS41LDRsLTIyLjUsNy4yNWMtMTAuNDA2MjUsMy4xODc1IC0xOS4wOTM3NSw5LjQzNzUgLTI1LjUsMTguMjVsLTIyLjUsNDIuNWwyNy4yNSwxNi43NWwxMi43NSwtMjR2MTE5LjI1YzAsNC40MDYyNSAyNS4wNjI1LDggNTYsOGMzMC45Mzc1LDAgNTYsLTMuNTkzNzUgNTYsLTh2LTExOS4yNWwxMi43NSwyNGwyNy4yNSwtMTYuNzVsLTIyLjUsLTQyLjVjLTYuNDA2MjUsLTguODEyNSAtMTUuMTU2MjUsLTE1LjA2MjUgLTI0Ljc1LC0xOC4yNWwtMjIuMjUsLTcuMjVjLTAuMTg3NSwwIC0xLjAzMTI1LDEuMzEyNSAtMiwyLjc1bDEuMjUsLTIuNWMwLDAgLTkuODQzNzUsLTQuMjUgLTI1Ljc1LC00LjI1ek05Niw4YzExLjQwNjI1LDAgMTguNDM3NSwyLjI1IDIxLDMuMjVjLTQuNDY4NzUsNS43NSAtMTEuNDA2MjUsMTIuNzUgLTIxLDEyLjc1Yy05LjQwNjI1LDAgLTE2LjQwNjI1LC03LjA2MjUgLTIwLjc1LC0xMi43NWMyLjg3NSwtMS4wNjI1IDkuODc1LC0zLjI1IDIwLjc1LC0zLjI1eiI+PC9wYXRoPjwvZz48L2c+PC9nPjwvc3ZnPg==)](https://wikijs.threadless.com)

Gold Tier Sponsors

GitHub Sponsors

Support this project by becoming a sponsor. Your name will show up in the Contribute page of all Wiki.js installations as well as here with a link to your website! [[Become a sponsor](https://github.com/users/NGPixel/sponsorship)]
Alexander Casassovici
(@alexksso)
Broxen
(@broxen)
Dacon
(@xDacon)
Maxime Pierre
(@DonNabla)
Jay Daley
(@JayDaley)
Oleksii
(@idokka)
Shane Kearney
(@shanekearney)
- Akira Suenami ([@a-suenami](https://github.com/a-suenami)) - Armin Reiter ([@arminreiter](https://github.com/arminreiter)) - Arnaud Marchand ([@snuids](https://github.com/snuids)) - Brian Douglass ([@bhdouglass](https://github.com/bhdouglass)) - Bryon Vandiver ([@asterick](https://github.com/asterick)) - Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer)) - Charlie Schliesser ([@charlie-s](https://github.com/charlie-s)) - Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC)) - Cole Manning ([@RVRX](https://github.com/RVRX)) - CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin)) - Daniel Horner ([@danhorner](https://github.com/danhorner)) - David Christian Holin ([@SirGibihm](https://github.com/SirGibihm)) - Dragan Espenschied ([@despens](https://github.com/despens)) - Elijah Zobenko ([@he110](https://github.com/he110)) - Emerson-Perna ([@Emerson-Perna](https://github.com/Emerson-Perna)) - Ernie ([@iamernie](https://github.com/iamernie)) - Fabio Ferrari ([@devxops](https://github.com/devxops)) - Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa)) - Florian Moss ([@florianmoss](https://github.com/florianmoss)) - GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen)) - HeavenBay ([@HeavenBay](https://github.com/heavenbay)) - HikaruEgashira ([@HikaruEgashira](https://github.com/HikaruEgashira)) - Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy)) - Jaimyn Mayer ([@jabelone](https://github.com/jabelone)) - Jay Lee ([@polyglotm](https://github.com/polyglotm)) - Kelly Wardrop ([@dropcoded](https://github.com/dropcoded)) - Loki ([@binaryloki](https://github.com/binaryloki)) - MaFarine ([@MaFarine](https://github.com/MaFarine)) - Marcilio Leite Neto ([@marclneto](https://github.com/marclneto)) - Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson)) - Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy)) - Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA)) - Mitchell Rowton ([@mrowton](https://github.com/mrowton)) - M. Scott Ford ([@mscottford](https://github.com/mscottford)) - Nick Halase ([@nhalase](https://github.com/nhalase)) - Nick Price ([@DominoTree](https://github.com/DominoTree)) - Nina Reynolds ([@cutecycle](https://github.com/cutecycle)) - Noel Cower ([@nilium](https://github.com/nilium)) - Oleksandr Koltsov ([@crambo](https://github.com/crambo)) - Phi Zeroth ([@phizeroth](https://github.com/phizeroth)) - Philipp Schmitt ([@pschmitt](https://github.com/pschmitt)) - Robert Lanzke ([@winkelement](https://github.com/winkelement)) - Ruizhe Li ([@liruizhe1995](https://github.com/liruizhe1995)) - Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth)) - Sean Coffey ([@seanecoffey](https://github.com/seanecoffey)) - Simon Ott ([@ottsimon](https://github.com/ottsimon)) - Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro)) - Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu)) - Tim Elmer ([@tim-elmer](https://github.com/tim-elmer)) - Tyler Denman ([@tylerguy](https://github.com/tylerguy)) - Victor Bilgin ([@vbilgin](https://github.com/vbilgin)) - VMO Solutions ([@vmosolutions](https://github.com/vmosolutions)) - YazMogg35 ([@YazMogg35](https://github.com/YazMogg35)) - Yu Yongwoo ([@uyu423](https://github.com/uyu423)) - ameyrakheja ([@ameyrakheja](https://github.com/ameyrakheja)) - aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani)) - aytaa ([@aytaa](https://github.com/aytaa)) - cesar ([@cesarnr21](https://github.com/cesarnr21)) - chaee ([@chaee](https://github.com/chaee)) - lwileczek ([@lwileczek](https://github.com/lwileczek)) - magicpotato ([@fortheday](https://github.com/fortheday)) - motoacs ([@motoacs](https://github.com/motoacs)) - muzian666 ([@muzian666](https://github.com/muzian666)) - rburckner ([@rburckner](https://github.com/rburckner)) - scorpion ([@scorpion](https://github.com/scorpion)) - valantien ([@valantien](https://github.com/valantien))

OpenCollective Sponsors

Support this project by becoming a sponsor. Your logo will show up in the Contribute page of all Wiki.js installations as well as here with a link to your website! [[Become a sponsor](https://opencollective.com/wikijs#sponsor)]

Patreon Backers

Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/requarks)]
- Aeternum - Al Romano - Alex Balabanov - Alex Milanov - Alex Zen - Arti Zirk - Ave - Brandon Curtis - Damien Hottelier - Daniel T. Holtzclaw - Dave 'Sri' Seah - djagoo - dz - Douglas Lassance - Ergoflix - Ernie Reid - Etienne - Flemis Jurgenheimer - Florent - Günter Pavlas - hong - Hope - Ian - Imari Childress - Iskander Callos - Josh Stewart - Justin Dunsworth - Keir - Loïc CRAMPON - Ludgeir Ibanez - Lyn Matten - Mads Rosendahl - Mark Mansur - Matt Gedigian - Mike Ditton - Nate Figz - Patryk - Paul O'Fallon - Philipp Schürch - Tracey Duffy - Quaxim - Richeir - Sergio Navarro Fernández - Shad Narcher - ShadowVoyd - SmartNET.works - Stepan Sokolovskyi - Zach Crawford - Zach Maynard - 张白驹

OpenCollective Backers

Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/wikijs#backer)]

Contributors

This project exists thanks to all the people who contribute. [[Contribute]](https://github.com/Requarks/wiki/blob/master/.github/CONTRIBUTING.md).

Special Thanks

![Browserstack](https://js.wiki/legacy/logo_browserstack.png) [Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools. ![Cloudflare](https://js.wiki/legacy/logo_cloudflare.png) [Cloudflare](https://www.cloudflare.com/) for providing their great CDN, SSL and advanced networking services. ![DigitalOcean](https://js.wiki/legacy/logo_digitalocean.png) [DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site and APIs. ![Icons8](https://static.requarks.io/logo/icons8-text-h40.png) [Icons8](https://icons8.com/) for providing access to their beautiful icon sets. ![Localazy](https://static.requarks.io/logo/localazy-h40.png) [Localazy](https://localazy.com/) for providing access to their great localization service. ![Lokalise](https://static.requarks.io/logo/lokalise-text-h40.png) [Lokalise](https://lokalise.com/) for providing access to their great localization tool. ![MacStadium](https://static.requarks.io/logo/macstadium-h40.png) [MacStadium](https://www.macstadium.com) for providing access to their Mac hardware in the cloud. ![Netlify](https://js.wiki/legacy/logo_netlify.png) [Netlify](https://www.netlify.com) for providing hosting for our website. ![ngrok](https://static.requarks.io/logo/ngrok-h40.png) [ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services. ![Porkbun](https://static.requarks.io/logo/porkbun.png) [Porkbun](https://www.porkbun.com) for providing domain registration services. ================================================ FILE: SECURITY.md ================================================ # Security Policy Wiki.js is built with security in mind. We try our absolute best to deliver secure and robust applications. However, like any software, there can be security bugs, either introduced by an update or by using an attack vector that wasn't considered when designing the software. If you find such vulnerability, it's important to disclose it in a quick and secure manner to the developers. Follow the instructions below to report a vulnerability. ## Supported Versions | Version | Supported | | ------- | ------------------ | | 2.x.x | :white_check_mark: | | 1.x.x | :x: | ## Reporting a Vulnerability > [!CAUTION] > **DO NOT CREATE A GITHUB ISSUE / DISCUSSION** to report a potential vulnerability / security problem. Instead, use the process below: Submit a Vulnerability Report by filling in the form on https://github.com/requarks/wiki/security/advisories/new Include as much details as possible, such as: - The version(s) of Wiki.js that are impacted - How to reproduce the vulnerability (step-by-step, screenshots or a video) - The platform / environment it occurs on (e.g. OS version, DB type + version, etc.) - Any potential fixes or reference code you think might be helpful in resolving the issue - Your GitHub username if you'd like to be included as a collaborator on the private fix branch The vulnerability will be investigated ASAP. If deemed valid, a draft security advisory will be created on GitHub and you will be included as a collaborator. A fix will be worked on in a private branch to resolves the issue. Once a fix is available, the advisory will be published. > [!NOTE] > There's no reward for submitting a report. As this is open source project and not corporate owned, we are not able to provide monetary rewards. You will however be credited as the bug reporter in the release notes. ================================================ FILE: client/.modernizrrc.js ================================================ module.exports = { classPrefix: 'mdz-', options: ['setClasses'], 'feature-detects': [ 'css/backdropfilter' ] } ================================================ FILE: client/client-app.js ================================================ /* global siteConfig */ import Vue from 'vue' import VueRouter from 'vue-router' import VueClipboards from 'vue-clipboards' import { ApolloClient } from 'apollo-client' import { BatchHttpLink } from 'apollo-link-batch-http' import { ApolloLink, split } from 'apollo-link' import { WebSocketLink } from 'apollo-link-ws' import { ErrorLink } from 'apollo-link-error' import { InMemoryCache } from 'apollo-cache-inmemory' import { getMainDefinition } from 'apollo-utilities' import VueApollo from 'vue-apollo' import Vuetify from 'vuetify/lib' import Velocity from 'velocity-animate' import Vuescroll from 'vuescroll/dist/vuescroll-native' import Hammer from 'hammerjs' import moment from 'moment-timezone' import VueMoment from 'vue-moment' import store from './store' import Cookies from 'js-cookie' // ==================================== // Load Modules // ==================================== import boot from './modules/boot' import localization from './modules/localization' // ==================================== // Load Helpers // ==================================== import helpers from './helpers' // ==================================== // Initialize Global Vars // ==================================== window.WIKI = null window.boot = boot window.Hammer = Hammer moment.locale(siteConfig.lang) store.commit('user/REFRESH_AUTH') // ==================================== // Initialize Apollo Client (GraphQL) // ==================================== const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql' const graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions' const graphQLLink = ApolloLink.from([ new ErrorLink(({ graphQLErrors, networkError }) => { if (graphQLErrors) { let isAuthError = false graphQLErrors.map(({ message, locations, path }) => { if (message === `Forbidden`) { isAuthError = true } console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) }) store.commit('showNotification', { style: 'red', message: isAuthError ? `You are not authorized to access this resource.` : `An unexpected error occurred.`, icon: 'alert' }) } if (networkError) { console.error(networkError) store.commit('showNotification', { style: 'red', message: `Network Error: ${networkError.message}`, icon: 'alert' }) } }), new BatchHttpLink({ includeExtensions: true, uri: graphQLEndpoint, credentials: 'include', fetch: async (uri, options) => { // Strip __typename fields from variables let body = JSON.parse(options.body) body = body.map(bd => { return ({ ...bd, variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value }) }) }) options.body = JSON.stringify(body) // Inject authentication token const jwtToken = Cookies.get('jwt') if (jwtToken) { options.headers.Authorization = `Bearer ${jwtToken}` } const resp = await fetch(uri, options) // Handle renewed JWT const newJWT = resp.headers.get('new-jwt') if (newJWT) { Cookies.set('jwt', newJWT, { expires: 365, secure: window.location.protocol === 'https:' }) } return resp } }) ]) const graphQLWSLink = new WebSocketLink({ uri: graphQLWSEndpoint, options: { reconnect: true, lazy: true, connectionParams: () => { const token = Cookies.get('jwt') return token ? { token } : {} } } }) window.graphQL = new ApolloClient({ link: split(({ query }) => { const { kind, operation } = getMainDefinition(query) return kind === 'OperationDefinition' && operation === 'subscription' }, graphQLWSLink, graphQLLink), cache: new InMemoryCache(), connectToDevTools: (process.env.node_env === 'development') }) // ==================================== // Initialize Vue Modules // ==================================== Vue.config.productionTip = false Vue.use(VueRouter) Vue.use(VueApollo) Vue.use(VueClipboards) Vue.use(localization.VueI18Next) Vue.use(helpers) Vue.use(Vuetify) Vue.use(VueMoment, { moment }) Vue.use(Vuescroll) Vue.prototype.Velocity = Velocity // ==================================== // Register Vue Components // ==================================== Vue.component('Admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue')) Vue.component('Comments', () => import(/* webpackChunkName: "comments" */ './components/comments.vue')) Vue.component('Editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue')) Vue.component('History', () => import(/* webpackChunkName: "history" */ './components/history.vue')) Vue.component('Loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue')) Vue.component('Login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue')) Vue.component('NavHeader', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue')) Vue.component('NewPage', () => import(/* webpackChunkName: "new-page" */ './components/new-page.vue')) Vue.component('Notify', () => import(/* webpackMode: "eager" */ './components/common/notify.vue')) Vue.component('NotFound', () => import(/* webpackChunkName: "not-found" */ './components/not-found.vue')) Vue.component('PageSelector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue')) Vue.component('PageSource', () => import(/* webpackChunkName: "source" */ './components/source.vue')) Vue.component('Profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue')) Vue.component('Register', () => import(/* webpackChunkName: "register" */ './components/register.vue')) Vue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue')) Vue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue')) Vue.component('Tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue')) Vue.component('Unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue')) Vue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue')) Vue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue')) Vue.component('Welcome', () => import(/* webpackChunkName: "welcome" */ './components/welcome.vue')) Vue.component('NavFooter', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue')) Vue.component('Page', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/page.vue')) let bootstrap = () => { // ==================================== // Notifications // ==================================== window.addEventListener('beforeunload', () => { store.dispatch('startLoading') }) const apolloProvider = new VueApollo({ defaultClient: window.graphQL }) // ==================================== // Bootstrap Vue // ==================================== const i18n = localization.init() let darkModeEnabled = siteConfig.darkMode if ((store.get('user/appearance') || '').length > 0) { darkModeEnabled = (store.get('user/appearance') === 'dark') } window.WIKI = new Vue({ el: '#root', components: {}, mixins: [helpers], apolloProvider, store, i18n, vuetify: new Vuetify({ rtl: siteConfig.rtl, theme: { dark: darkModeEnabled } }), mounted () { this.$moment.locale(siteConfig.lang) if ((store.get('user/dateFormat') || '').length > 0) { this.$moment.updateLocale(this.$moment.locale(), { longDateFormat: { 'L': store.get('user/dateFormat') } }) } if ((store.get('user/timezone') || '').length > 0) { this.$moment.tz.setDefault(store.get('user/timezone')) } } }) // ---------------------------------- // Dispatch boot ready // ---------------------------------- window.boot.notify('vue') } window.boot.onDOMReady(bootstrap) ================================================ FILE: client/client-setup.js ================================================ /* eslint-disable import/first */ import Vue from 'vue' import Vuetify from 'vuetify/lib' import boot from './modules/boot' /* eslint-enable import/first */ window.WIKI = null window.boot = boot Vue.use(Vuetify) Vue.component('setup', () => import(/* webpackMode: "eager" */ './components/setup.vue')) let bootstrap = () => { window.WIKI = new Vue({ el: '#root', vuetify: new Vuetify() }) } window.boot.onDOMReady(bootstrap) ================================================ FILE: client/components/admin/admin-analytics.vue ================================================ ================================================ FILE: client/components/admin/admin-api-create.vue ================================================ ================================================ FILE: client/components/admin/admin-api.vue ================================================ ================================================ FILE: client/components/admin/admin-auth.vue ================================================ ================================================ FILE: client/components/admin/admin-comments.vue ================================================ ================================================ FILE: client/components/admin/admin-contribute.vue ================================================ ================================================ FILE: client/components/admin/admin-dashboard.vue ================================================ ================================================ FILE: client/components/admin/admin-dev-flags.vue ================================================ ================================================ FILE: client/components/admin/admin-editor.vue ================================================ ================================================ FILE: client/components/admin/admin-extensions.vue ================================================ ================================================ FILE: client/components/admin/admin-general.vue ================================================ ================================================ FILE: client/components/admin/admin-groups-edit-permissions.vue ================================================ ================================================ FILE: client/components/admin/admin-groups-edit-rules.vue ================================================ ================================================ FILE: client/components/admin/admin-groups-edit-users.vue ================================================ ================================================ FILE: client/components/admin/admin-groups-edit.vue ================================================ ================================================ FILE: client/components/admin/admin-groups.vue ================================================ ================================================ FILE: client/components/admin/admin-locale.vue ================================================ ================================================ FILE: client/components/admin/admin-logging-console.vue ================================================ ================================================ FILE: client/components/admin/admin-logging.vue ================================================ ================================================ FILE: client/components/admin/admin-mail.vue ================================================ ================================================ FILE: client/components/admin/admin-navigation.vue ================================================ ================================================ FILE: client/components/admin/admin-pages-edit.vue ================================================ ================================================ FILE: client/components/admin/admin-pages-visualize.vue ================================================ ================================================ FILE: client/components/admin/admin-pages.vue ================================================ ================================================ FILE: client/components/admin/admin-rendering.vue ================================================ ================================================ FILE: client/components/admin/admin-search.vue ================================================ ================================================ FILE: client/components/admin/admin-security.vue ================================================ ================================================ FILE: client/components/admin/admin-ssl.vue ================================================ ================================================ FILE: client/components/admin/admin-stats.vue ================================================ ================================================ FILE: client/components/admin/admin-storage.vue ================================================ ================================================ FILE: client/components/admin/admin-system.vue ================================================ ================================================ FILE: client/components/admin/admin-tags.vue ================================================ ================================================ FILE: client/components/admin/admin-theme.vue ================================================ ================================================ FILE: client/components/admin/admin-users-create.vue ================================================ ================================================ FILE: client/components/admin/admin-users-edit.vue ================================================ ================================================ FILE: client/components/admin/admin-users.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-auth.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-cache.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-content.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-export.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-importv1.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities-telemetry.vue ================================================ ================================================ FILE: client/components/admin/admin-utilities.vue ================================================ ================================================ FILE: client/components/admin/admin-webhooks.vue ================================================ ================================================ FILE: client/components/admin.vue ================================================ ================================================ FILE: client/components/comments.vue ================================================ ================================================ FILE: client/components/common/duration-picker.vue ================================================ ================================================ FILE: client/components/common/loader.vue ================================================ ================================================ FILE: client/components/common/nav-header.vue ================================================ ================================================ FILE: client/components/common/notify.vue ================================================ ================================================ FILE: client/components/common/page-convert.vue ================================================ ================================================ FILE: client/components/common/page-delete.vue ================================================ ================================================ FILE: client/components/common/page-selector.vue ================================================ ================================================ FILE: client/components/common/password-strength.vue ================================================ ================================================ FILE: client/components/common/search-results.vue ================================================ ================================================ FILE: client/components/common/social-sharing.vue ================================================ ================================================ FILE: client/components/common/user-search.vue ================================================ ================================================ FILE: client/components/common/v-card-chin.vue ================================================ ================================================ FILE: client/components/common/v-card-info.vue ================================================ ================================================ FILE: client/components/editor/api/server-selector.vue ================================================ ================================================ FILE: client/components/editor/ckeditor/conflict.vue ================================================ ================================================ FILE: client/components/editor/common/cmFold.js ================================================ // Header matching code by CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/LICENSE import CodeMirror from 'codemirror' const maxDepth = 100 const codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/ const codeBlockEndMatch = /^`{3}$/ export default { register(lang) { CodeMirror.registerHelper('fold', lang, foldHandler) } } function foldHandler (cm, start) { const firstLine = cm.getLine(start.line) const lastLineNo = cm.lastLine() let end function isHeader(lineNo) { const tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)) return tokentype && /\bheader\b/.test(tokentype) } function headerLevel(lineNo, line, nextLine) { let match = line && line.match(/^#+/) if (match && isHeader(lineNo)) return match[0].length match = nextLine && nextLine.match(/^[=-]+\s*$/) if (match && isHeader(lineNo + 1)) return nextLine[0] === '=' ? 1 : 2 return maxDepth } // -> CODE BLOCK if (codeBlockStartMatch.test(cm.getLine(start.line))) { end = start.line let nextNextLine = cm.getLine(end + 1) while (end < lastLineNo) { if (codeBlockEndMatch.test(nextNextLine)) { end++ break } end++ nextNextLine = cm.getLine(end + 1) } } else { // -> HEADER let nextLine = cm.getLine(start.line + 1) const level = headerLevel(start.line, firstLine, nextLine) if (level === maxDepth) return undefined end = start.line let nextNextLine = cm.getLine(end + 2) while (end < lastLineNo) { if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break ++end nextLine = nextNextLine nextNextLine = cm.getLine(end + 2) } } return { from: CodeMirror.Pos(start.line, firstLine.length), to: CodeMirror.Pos(end, cm.getLine(end).length) } } ================================================ FILE: client/components/editor/common/katex.js ================================================ // Test if potential opening or closing delimieter // Assumes that there is a "$" at state.src[pos] function isValidDelim (state, pos) { let prevChar let nextChar let max = state.posMax let canOpen = true let canClose = true prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1 nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1 // Check non-whitespace conditions for opening and closing, and // check that closing delimeter isn't followed by a number if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { canClose = false } if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { canOpen = false } return { canOpen: canOpen, canClose: canClose } } export default { katexInline (state, silent) { let start, match, token, res, pos if (state.src[state.pos] !== '$') { return false } res = isValidDelim(state, state.pos) if (!res.canOpen) { if (!silent) { state.pending += '$' } state.pos += 1 return true } // First check for and bypass all properly escaped delimieters // This loop will assume that the first leading backtick can not // be the first character in state.src, which is known since // we have found an opening delimieter already. start = state.pos + 1 match = start while ((match = state.src.indexOf('$', match)) !== -1) { // Found potential $, look for escapes, pos will point to // first non escape when complete pos = match - 1 while (state.src[pos] === '\\') { pos -= 1 } // Even number of escapes, potential closing delimiter found if (((match - pos) % 2) === 1) { break } match += 1 } // No closing delimter found. Consume $ and continue. if (match === -1) { if (!silent) { state.pending += '$' } state.pos = start return true } // Check if we have empty content, ie: $$. Do not parse. if (match - start === 0) { if (!silent) { state.pending += '$$' } state.pos = start + 1 return true } // Check for valid closing delimiter res = isValidDelim(state, match) if (!res.canClose) { if (!silent) { state.pending += '$' } state.pos = start return true } if (!silent) { token = state.push('katex_inline', 'math', 0) token.markup = '$' token.content = state.src // Extract the math part without the $ .slice(start, match) // Escape the curly braces since they will be interpreted as // attributes by markdown-it-attrs (the "curly_attributes" // core rule) .replaceAll("{", "{{") .replaceAll("}", "}}") } state.pos = match + 1 return true }, katexBlock (state, start, end, silent) { let firstLine; let lastLine; let next; let lastPos; let found = false; let token let pos = state.bMarks[start] + state.tShift[start] let max = state.eMarks[start] if (pos + 2 > max) { return false } if (state.src.slice(pos, pos + 2) !== '$$') { return false } pos += 2 firstLine = state.src.slice(pos, max) if (silent) { return true } if (firstLine.trim().slice(-2) === '$$') { // Single line expression firstLine = firstLine.trim().slice(0, -2) found = true } for (next = start; !found;) { next++ if (next >= end) { break } pos = state.bMarks[next] + state.tShift[next] max = state.eMarks[next] if (pos < max && state.tShift[next] < state.blkIndent) { // non-empty line with negative indent should stop the list: break } if (state.src.slice(pos, max).trim().slice(-2) === '$$') { lastPos = state.src.slice(0, max).lastIndexOf('$$') lastLine = state.src.slice(pos, lastPos) found = true } } state.line = next + 1 token = state.push('katex_block', 'math', 0) token.block = true token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : '') token.map = [ start, state.line ] token.markup = '$$' return true } } ================================================ FILE: client/components/editor/editor-api.vue ================================================ ================================================ FILE: client/components/editor/editor-asciidoc.vue ================================================ ================================================ FILE: client/components/editor/editor-ckeditor.vue ================================================ ================================================ FILE: client/components/editor/editor-code.vue ================================================ ================================================ FILE: client/components/editor/editor-markdown.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-blocks.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-conflict.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-drawio.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-editorselect.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-media.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-properties.vue ================================================ ================================================ FILE: client/components/editor/editor-modal-unsaved.vue ================================================ ================================================ FILE: client/components/editor/editor-redirect.vue ================================================ ================================================ FILE: client/components/editor/markdown/help.vue ================================================ ================================================ FILE: client/components/editor/markdown/plantuml.js ================================================ const pako = require('pako') // ------------------------------------ // Markdown - PlantUML Preprocessor // ------------------------------------ module.exports = { init (mdinst, conf) { mdinst.use((md, opts) => { const openMarker = opts.openMarker || '```plantuml' const openChar = openMarker.charCodeAt(0) const closeMarker = opts.closeMarker || '```' const closeChar = closeMarker.charCodeAt(0) const imageFormat = opts.imageFormat || 'svg' const server = opts.server || 'https://plantuml.requarks.io' md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => { let nextLine let markup let params let token let i let autoClosed = false let start = state.bMarks[startLine] + state.tShift[startLine] let max = state.eMarks[startLine] // Check out the first character quickly, // this should filter out most of non-uml blocks // if (openChar !== state.src.charCodeAt(start)) { return false } // Check out the rest of the marker string // for (i = 0; i < openMarker.length; ++i) { if (openMarker[i] !== state.src[start + i]) { return false } } markup = state.src.slice(start, start + i) params = state.src.slice(start + i, max) // Since start is found, we can report success here in validation mode // if (silent) { return true } // Search for the end of the block // nextLine = startLine for (;;) { nextLine++ if (nextLine >= endLine) { // unclosed block should be autoclosed by end of document. // also block seems to be autoclosed by end of parent break } start = state.bMarks[nextLine] + state.tShift[nextLine] max = state.eMarks[nextLine] if (start < max && state.sCount[nextLine] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test break } if (closeChar !== state.src.charCodeAt(start)) { // didn't find the closing fence continue } if (state.sCount[nextLine] > state.sCount[startLine]) { // closing fence should not be indented with respect of opening fence continue } var closeMarkerMatched = true for (i = 0; i < closeMarker.length; ++i) { if (closeMarker[i] !== state.src[start + i]) { closeMarkerMatched = false break } } if (!closeMarkerMatched) { continue } // make sure tail has spaces only if (state.skipSpaces(start + i) < max) { continue } // found! autoClosed = true break } const contents = state.src .split('\n') .slice(startLine + 1, nextLine) .join('\n') // We generate a token list for the alt property, to mimic what the image parser does. let altToken = [] // Remove leading space if any. let alt = params ? params.slice(1) : 'uml diagram' state.md.inline.parse( alt, state.md, state.env, altToken ) var zippedCode = encode64(pako.deflate('@startuml\n' + contents + '\n@enduml', { to: 'string' })) token = state.push('uml_diagram', 'img', 0) // alt is constructed from children. No point in populating it here. token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram'] ] token.block = true token.children = altToken token.info = params token.map = [ startLine, nextLine ] token.markup = markup state.line = nextLine + (autoClosed ? 1 : 0) return true }, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) md.renderer.rules.uml_diagram = md.renderer.rules.image }, { openMarker: conf.openMarker, closeMarker: conf.closeMarker, imageFormat: conf.imageFormat, server: conf.server }) } } function encode64 (data) { let r = '' for (let i = 0; i < data.length; i += 3) { if (i + 2 === data.length) { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0) } else if (i + 1 === data.length) { r += append3bytes(data.charCodeAt(i), 0, 0) } else { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2)) } } return r } function append3bytes (b1, b2, b3) { let c1 = b1 >> 2 let c2 = ((b1 & 0x3) << 4) | (b2 >> 4) let c3 = ((b2 & 0xF) << 2) | (b3 >> 6) let c4 = b3 & 0x3F let r = '' r += encode6bit(c1 & 0x3F) r += encode6bit(c2 & 0x3F) r += encode6bit(c3 & 0x3F) r += encode6bit(c4 & 0x3F) return r } function encode6bit(raw) { let b = raw if (b < 10) { return String.fromCharCode(48 + b) } b -= 10 if (b < 26) { return String.fromCharCode(65 + b) } b -= 26 if (b < 26) { return String.fromCharCode(97 + b) } b -= 26 if (b === 0) { return '-' } if (b === 1) { return '_' } return '?' } ================================================ FILE: client/components/editor/markdown/tabset.js ================================================ import cash from 'cash-dom' import _ from 'lodash' export default { format () { for (let i = 1; i < 6; i++) { cash(`.editor-markdown-preview-content h${i}.tabset`).each((idx, elm) => { elm.innerHTML = 'Tabset ( rendered upon saving )' cash(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => { hd.classList.add('tabset-header') cash(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).wrapAll('
') }) }) } } } ================================================ FILE: client/components/editor.vue ================================================ ================================================ FILE: client/components/history.vue ================================================ ================================================ FILE: client/components/login.vue ================================================ ================================================ FILE: client/components/new-page.vue ================================================ ================================================ FILE: client/components/not-found.vue ================================================ ================================================ FILE: client/components/profile/comments.vue ================================================ ================================================ FILE: client/components/profile/pages.vue ================================================ ================================================ FILE: client/components/profile/profile.vue ================================================ ================================================ FILE: client/components/profile.vue ================================================ ================================================ FILE: client/components/register.vue ================================================ ================================================ FILE: client/components/setup.vue ================================================ ================================================ FILE: client/components/source.vue ================================================ ================================================ FILE: client/components/tags.vue ================================================ ================================================ FILE: client/components/unauthorized.vue ================================================ ================================================ FILE: client/components/welcome.vue ================================================ ================================================ FILE: client/graph/admin/analytics/analytics-mutation-save-providers.gql ================================================ mutation($providers: [AnalyticsProviderInput]!) { analytics { updateProviders(providers: $providers) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/analytics/analytics-query-providers.gql ================================================ query { analytics { providers { isEnabled key title description isAvailable logo website config { key value } } } } ================================================ FILE: client/graph/admin/auth/auth-query-groups.gql ================================================ query { groups { list { id name } } } ================================================ FILE: client/graph/admin/auth/auth-query-host.gql ================================================ { site { config { host } } } ================================================ FILE: client/graph/admin/auth/auth-query-strategies.gql ================================================ query { authentication { strategies { isEnabled key title description isAvailable useForm logo website config { key value } selfRegistration domainWhitelist autoEnrollGroups } } } ================================================ FILE: client/graph/admin/contribute/contribute-query-contributors.gql ================================================ query { contribute { contributors { company currency description id image name profile tier totalDonated twitter website } } } ================================================ FILE: client/graph/admin/dashboard/dashboard-query-stats.gql ================================================ query { system { info { currentVersion latestVersion groupsTotal pagesTotal usersTotal tagsTotal } } } ================================================ FILE: client/graph/admin/dev/dev-mutation-save-flags.gql ================================================ mutation ( $flags: [SystemFlagInput]! ) { system { updateFlags( flags: $flags ) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/dev/dev-query-flags.gql ================================================ { system { flags { key value } } } ================================================ FILE: client/graph/admin/groups/groups-mutation-assign.gql ================================================ mutation ($groupId: Int!, $userId: Int!) { groups { assignUser(groupId: $groupId, userId: $userId) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/groups/groups-mutation-create.gql ================================================ mutation ($name: String!) { groups { create(name: $name) { responseResult { succeeded errorCode slug message } group { id name createdAt updatedAt } } } } ================================================ FILE: client/graph/admin/groups/groups-mutation-unassign.gql ================================================ mutation ($groupId: Int!, $userId: Int!) { groups { unassignUser(groupId: $groupId, userId: $userId) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/groups/groups-query-list.gql ================================================ query { groups { list { id name isSystem userCount createdAt updatedAt } } } ================================================ FILE: client/graph/admin/locale/locale-mutation-download.gql ================================================ mutation($locale: String!) { localization { downloadLocale(locale: $locale) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/locale/locale-mutation-save.gql ================================================ mutation($locale: String!, $autoUpdate: Boolean!, $namespacing: Boolean!, $namespaces: [String]!) { localization { updateLocale(locale: $locale, autoUpdate: $autoUpdate, namespacing: $namespacing, namespaces: $namespaces) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/locale/locale-query-list.gql ================================================ { localization { locales { availability code createdAt isInstalled installDate isRTL name nativeName updatedAt } config { locale autoUpdate namespacing namespaces } } } ================================================ FILE: client/graph/admin/logging/logging-mutation-save-loggers.gql ================================================ mutation($loggers: [LoggerInput]) { logging { updateLoggers(loggers: $loggers) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/logging/logging-query-loggers.gql ================================================ query { logging { loggers(orderBy: "title ASC") { isEnabled key title description logo website level config { key value } } } } ================================================ FILE: client/graph/admin/logging/logging-subscription-livetrail.gql ================================================ subscription { loggingLiveTrail { level output timestamp } } ================================================ FILE: client/graph/admin/mail/mail-mutation-save-config.gql ================================================ mutation ( $senderName: String! $senderEmail: String! $host: String! $port: Int! $name: String! $secure: Boolean! $verifySSL: Boolean! $user: String! $pass: String! $useDKIM: Boolean! $dkimDomainName: String! $dkimKeySelector: String! $dkimPrivateKey: String! ) { mail { updateConfig( senderName: $senderName senderEmail: $senderEmail host: $host port: $port name: $name secure: $secure verifySSL: $verifySSL user: $user pass: $pass useDKIM: $useDKIM dkimDomainName: $dkimDomainName dkimKeySelector: $dkimKeySelector dkimPrivateKey: $dkimPrivateKey ) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/mail/mail-mutation-sendtest.gql ================================================ mutation ($recipientEmail: String!) { mail { sendTest(recipientEmail: $recipientEmail) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/mail/mail-query-config.gql ================================================ { mail { config { senderName senderEmail host port name secure verifySSL user pass useDKIM dkimDomainName dkimKeySelector dkimPrivateKey } } } ================================================ FILE: client/graph/admin/pages/pages-query-list.gql ================================================ query { pages { list { id locale path title description contentType isPublished isPrivate privateNS createdAt updatedAt } } } ================================================ FILE: client/graph/admin/pages/pages-query-single.gql ================================================ query($id: Int!) { pages { single(id:$id) { id path hash title description isPrivate isPublished privateNS publishStartDate publishEndDate contentType createdAt updatedAt editor locale authorId authorName authorEmail creatorId creatorName creatorEmail } } } ================================================ FILE: client/graph/admin/rendering/rendering-mutation-save-renderers.gql ================================================ mutation($renderers: [RendererInput]) { rendering { updateRenderers(renderers: $renderers) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/rendering/rendering-query-renderers.gql ================================================ { rendering { renderers { isEnabled key title description icon dependsOn input output config { key value } } } } ================================================ FILE: client/graph/admin/search/search-mutation-rebuild-index.gql ================================================ mutation { search { rebuildIndex { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/search/search-mutation-save-engines.gql ================================================ mutation($engines: [SearchEngineInput]) { search { updateSearchEngines(engines: $engines) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/search/search-query-engines.gql ================================================ query { search { searchEngines(orderBy: "title") { isEnabled key title description logo website isAvailable config { key value } } } } ================================================ FILE: client/graph/admin/storage/storage-mutation-executeaction.gql ================================================ mutation($targetKey: String!, $handler: String!) { storage { executeAction(targetKey: $targetKey, handler: $handler) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/storage/storage-mutation-save-targets.gql ================================================ mutation($targets: [StorageTargetInput]!) { storage { updateTargets(targets: $targets) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/storage/storage-query-status.gql ================================================ query { storage { status { key title status message lastAttempt } } } ================================================ FILE: client/graph/admin/storage/storage-query-targets.gql ================================================ query { storage { targets { isAvailable isEnabled key title description logo website supportedModes mode hasSchedule syncInterval syncIntervalDefault config { key value } actions { handler label hint } } } } ================================================ FILE: client/graph/admin/system/system-mutation-upgrade.gql ================================================ mutation { system { performUpgrade { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/system/system-query-info.gql ================================================ query { system { info { configFile cpuCores currentVersion dbHost dbType dbVersion hostname latestVersion latestVersionReleaseDate nodeVersion operatingSystem platform ramTotal upgradeCapable workingDirectory } } } ================================================ FILE: client/graph/admin/theme/theme-mutation-save.gql ================================================ mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $tocPosition: String, $injectCSS: String, $injectHead: String, $injectBody: String) { theming { setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, tocPosition: $tocPosition, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/theme/theme-query-config.gql ================================================ query { theming { config { theme iconset darkMode tocPosition injectCSS injectHead injectBody } } } ================================================ FILE: client/graph/admin/users/users-mutation-create.gql ================================================ mutation ($providerKey: String!, $email: String!, $name: String!, $passwordRaw: String, $groups: [Int]!, $mustChangePassword: Boolean, $sendWelcomeEmail: Boolean) { users { create(providerKey: $providerKey, email: $email, name: $name, passwordRaw: $passwordRaw, groups: $groups, mustChangePassword: $mustChangePassword, sendWelcomeEmail: $sendWelcomeEmail) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/users/users-query-groups.gql ================================================ query { groups { list { id name isSystem } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-auth-regencerts.gql ================================================ mutation { authentication { regenerateCertificates { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-auth-resetguest.gql ================================================ mutation { authentication { resetGuestUser { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-cache-flushcache.gql ================================================ mutation { pages { flushCache { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-cache-flushuploads.gql ================================================ mutation { assets { flushTempUploads { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-content-migratelocale.gql ================================================ mutation($sourceLocale: String!, $targetLocale: String!) { pages { migrateToLocale(sourceLocale: $sourceLocale, targetLocale: $targetLocale) { responseResult { succeeded errorCode slug message } count } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-content-rebuildtree.gql ================================================ mutation { pages { rebuildTree { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-importv1-users.gql ================================================ mutation($mongoDbConnString: String!, $groupMode: SystemImportUsersGroupMode!) { system { importUsersFromV1(mongoDbConnString: $mongoDbConnString, groupMode: $groupMode) { responseResult { succeeded errorCode slug message } usersCount groupsCount failed { provider email error } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-telemetry-resetid.gql ================================================ mutation { system { resetTelemetryClientId { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-mutation-telemetry-set.gql ================================================ mutation($enabled: Boolean!) { system { setTelemetry(enabled: $enabled) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/admin/utilities/utilities-query-telemetry.gql ================================================ query { system { info { telemetry telemetryClientId } } } ================================================ FILE: client/graph/common/common-localization-query-translations.gql ================================================ query($locale: String!, $namespace: String!) { localization { translations(locale:$locale, namespace:$namespace) { key value } } } ================================================ FILE: client/graph/common/common-pages-mutation-delete.gql ================================================ mutation($id: Int!) { pages { delete(id: $id) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/common/common-pages-mutation-move.gql ================================================ mutation($id: Int!, $destinationPath: String!, $destinationLocale: String!) { pages { move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/common/common-pages-query-list.gql ================================================ query ($limit: Int, $orderBy: PageOrderBy, $orderByDirection: PageOrderByDirection, $tags: [String!], $locale: String) { pages { list(limit: $limit, orderBy: $orderBy, orderByDirection: $orderByDirection, tags: $tags, locale: $locale) { id locale path title description createdAt updatedAt tags } } } ================================================ FILE: client/graph/common/common-pages-query-search.gql ================================================ query ($query: String!) { pages { search(query:$query) { results { id title description path locale } suggestions totalHits } } } ================================================ FILE: client/graph/common/common-pages-query-tags.gql ================================================ query { pages { tags { tag title } } } ================================================ FILE: client/graph/common/common-pages-query-tree.gql ================================================ query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) { pages { tree(parent: $parent, mode: $mode, locale: $locale) { id path title isFolder pageId parent } } } ================================================ FILE: client/graph/editor/editor-media-mutation-asset-delete.gql ================================================ mutation ($id: Int!) { assets { deleteAsset(id: $id) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/editor/editor-media-mutation-asset-rename.gql ================================================ mutation ($id: Int!, $filename: String!) { assets { renameAsset(id:$id, filename: $filename) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/editor/editor-media-mutation-folder-create.gql ================================================ mutation ($parentFolderId: Int!, $slug: String!) { assets { createFolder(parentFolderId:$parentFolderId, slug: $slug) { responseResult { succeeded errorCode slug message } } } } ================================================ FILE: client/graph/editor/editor-media-query-folder-list.gql ================================================ query ($parentFolderId: Int!) { assets { folders(parentFolderId:$parentFolderId) { id name slug } } } ================================================ FILE: client/graph/editor/editor-media-query-list.gql ================================================ query ($folderId: Int!, $kind: AssetKind!) { assets { list(folderId:$folderId, kind: $kind) { id filename ext kind mime fileSize createdAt updatedAt } } } ================================================ FILE: client/graph/login/login-mutation-changepassword.gql ================================================ mutation($continuationToken: String!, $newPassword: String!) { authentication { loginChangePassword(continuationToken: $continuationToken, newPassword: $newPassword) { responseResult { succeeded errorCode slug message } jwt } } } ================================================ FILE: client/graph/login/login-mutation-login.gql ================================================ mutation($username: String!, $password: String!, $strategy: String!) { authentication { login(username: $username, password: $password, strategy: $strategy) { responseResult { succeeded errorCode slug message } jwt mustChangePwd mustProvideTFA continuationToken } } } ================================================ FILE: client/graph/login/login-mutation-tfa.gql ================================================ mutation($continuationToken: String!, $securityCode: String!) { authentication { loginTFA(continuationToken: $continuationToken, securityCode: $securityCode) { responseResult { succeeded errorCode slug message } jwt } } } ================================================ FILE: client/graph/login/login-query-strategies.gql ================================================ query { authentication { strategies( isEnabled: true ) { key title useForm icon color selfRegistration } } } ================================================ FILE: client/graph/register/register-mutation-create.gql ================================================ mutation($email: String!, $password: String!, $name: String!) { authentication { register(email: $email, password: $password, name: $name) { responseResult { succeeded errorCode slug message } jwt } } } ================================================ FILE: client/helpers/compatibility.js ================================================ // ======================================= // Fetch polyfill // ======================================= // Requirement: Safari 9 and below, IE 11 and below if (!window.fetch) { require('whatwg-fetch') } ================================================ FILE: client/helpers/index.js ================================================ import filesize from 'filesize.js' import _ from 'lodash' /* global siteConfig */ const helpers = { /** * Convert bytes to humanized form * @param {number} rawSize Size in bytes * @returns {string} Humanized file size */ filesize (rawSize) { return _.toUpper(filesize(rawSize)) }, /** * Convert raw path to safe path * @param {string} rawPath Raw path * @returns {string} Safe path */ makeSafePath (rawPath) { let rawParts = _.split(_.trim(rawPath), '/') rawParts = _.map(rawParts, (r) => { return _.kebabCase(_.deburr(_.trim(r))) }) return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/') }, resolvePath (path) { if (_.startsWith(path, '/')) { path = path.substring(1) } return `${siteConfig.path}${path}` }, /** * Set Input Selection * @param {DOMElement} input The input element * @param {number} startPos The starting position * @param {nunber} endPos The ending position */ setInputSelection (input, startPos, endPos) { input.focus() if (typeof input.selectionStart !== 'undefined') { input.selectionStart = startPos input.selectionEnd = endPos } else if (document.selection && document.selection.createRange) { // IE branch input.select() var range = document.selection.createRange() range.collapse(true) range.moveEnd('character', endPos) range.moveStart('character', startPos) range.select() } } } export default { install(Vue) { Vue.$helpers = helpers Object.defineProperties(Vue.prototype, { $helpers: { get() { return helpers } } }) } } ================================================ FILE: client/index-app.js ================================================ require('core-js/stable') require('regenerator-runtime/runtime') /* global siteConfig */ /* eslint-disable no-unused-expressions */ switch (window.document.documentElement.lang) { case 'ar': case 'fa': import(/* webpackChunkName: "fonts-arabic" */ './scss/fonts/arabic.scss') break default: import(/* webpackChunkName: "fonts-default" */ './scss/fonts/default.scss') break } require('modernizr') require('./scss/app.scss') import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/scss/app.scss') import(/* webpackChunkName: "mdi" */ '@mdi/font/css/materialdesignicons.css') require('./helpers/compatibility.js') require('./client-app.js') import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/js/app.js') ================================================ FILE: client/index-legacy.js ================================================ require('./scss/legacy.scss') require('./scss/fonts/default.scss') window.WIKI = null ================================================ FILE: client/index-setup.js ================================================ require('core-js/stable') require('regenerator-runtime/runtime') /* eslint-disable no-unused-expressions */ require('./scss/app.scss') import(/* webpackChunkName: "mdi" */ '@mdi/font/css/materialdesignicons.css') require('./helpers/compatibility.js') require('./client-setup.js') ================================================ FILE: client/libs/animate/animate.scss ================================================ @charset "UTF-8"; /*! * animate.css -http://daneden.me/animate * Version - 3.5.1 * Licensed under the MIT license - http://opensource.org/licenses/MIT * * Copyright (c) 2016 Daniel Eden */ .animated { -webkit-animation-duration: 1s; animation-duration: 1s; -webkit-animation-fill-mode: both; animation-fill-mode: both; &.infinite { -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; } &.hinge { -webkit-animation-duration: 2s; animation-duration: 2s; } &.flipOutX, &.flipOutY, &.bounceIn, &.bounceOut { -webkit-animation-duration: .75s; animation-duration: .75s; } } @-webkit-keyframes bounce { from, 20%, 53%, 80%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } 40%, 43% { -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); -webkit-transform: translate3d(0, -30px, 0); transform: translate3d(0, -30px, 0); } 70% { -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); -webkit-transform: translate3d(0, -15px, 0); transform: translate3d(0, -15px, 0); } 90% { -webkit-transform: translate3d(0, -4px, 0); transform: translate3d(0, -4px, 0); } } @keyframes bounce { from, 20%, 53%, 80%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } 40%, 43% { -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); -webkit-transform: translate3d(0, -30px, 0); transform: translate3d(0, -30px, 0); } 70% { -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); -webkit-transform: translate3d(0, -15px, 0); transform: translate3d(0, -15px, 0); } 90% { -webkit-transform: translate3d(0, -4px, 0); transform: translate3d(0, -4px, 0); } } .bounce { -webkit-animation-name: bounce; animation-name: bounce; -webkit-transform-origin: center bottom; transform-origin: center bottom; } @-webkit-keyframes flash { from, 50%, to { opacity: 1; } 25%, 75% { opacity: 0; } } @keyframes flash { from, 50%, to { opacity: 1; } 25%, 75% { opacity: 0; } } .flash { -webkit-animation-name: flash; animation-name: flash; } /* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ @-webkit-keyframes pulse { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 50% { -webkit-transform: scale3d(1.05, 1.05, 1.05); transform: scale3d(1.05, 1.05, 1.05); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } @keyframes pulse { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 50% { -webkit-transform: scale3d(1.05, 1.05, 1.05); transform: scale3d(1.05, 1.05, 1.05); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } .pulse { -webkit-animation-name: pulse; animation-name: pulse; } @-webkit-keyframes rubberBand { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 30% { -webkit-transform: scale3d(1.25, 0.75, 1); transform: scale3d(1.25, 0.75, 1); } 40% { -webkit-transform: scale3d(0.75, 1.25, 1); transform: scale3d(0.75, 1.25, 1); } 50% { -webkit-transform: scale3d(1.15, 0.85, 1); transform: scale3d(1.15, 0.85, 1); } 65% { -webkit-transform: scale3d(0.95, 1.05, 1); transform: scale3d(0.95, 1.05, 1); } 75% { -webkit-transform: scale3d(1.05, 0.95, 1); transform: scale3d(1.05, 0.95, 1); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } @keyframes rubberBand { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 30% { -webkit-transform: scale3d(1.25, 0.75, 1); transform: scale3d(1.25, 0.75, 1); } 40% { -webkit-transform: scale3d(0.75, 1.25, 1); transform: scale3d(0.75, 1.25, 1); } 50% { -webkit-transform: scale3d(1.15, 0.85, 1); transform: scale3d(1.15, 0.85, 1); } 65% { -webkit-transform: scale3d(0.95, 1.05, 1); transform: scale3d(0.95, 1.05, 1); } 75% { -webkit-transform: scale3d(1.05, 0.95, 1); transform: scale3d(1.05, 0.95, 1); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } .rubberBand { -webkit-animation-name: rubberBand; animation-name: rubberBand; } @-webkit-keyframes shake { from, to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } 10%, 30%, 50%, 70%, 90% { -webkit-transform: translate3d(-10px, 0, 0); transform: translate3d(-10px, 0, 0); } 20%, 40%, 60%, 80% { -webkit-transform: translate3d(10px, 0, 0); transform: translate3d(10px, 0, 0); } } @keyframes shake { from, to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } 10%, 30%, 50%, 70%, 90% { -webkit-transform: translate3d(-10px, 0, 0); transform: translate3d(-10px, 0, 0); } 20%, 40%, 60%, 80% { -webkit-transform: translate3d(10px, 0, 0); transform: translate3d(10px, 0, 0); } } .shake { -webkit-animation-name: shake; animation-name: shake; } @-webkit-keyframes headShake { 0% { -webkit-transform: translateX(0); transform: translateX(0); } 6.5% { -webkit-transform: translateX(-6px) rotateY(-9deg); transform: translateX(-6px) rotateY(-9deg); } 18.5% { -webkit-transform: translateX(5px) rotateY(7deg); transform: translateX(5px) rotateY(7deg); } 31.5% { -webkit-transform: translateX(-3px) rotateY(-5deg); transform: translateX(-3px) rotateY(-5deg); } 43.5% { -webkit-transform: translateX(2px) rotateY(3deg); transform: translateX(2px) rotateY(3deg); } 50% { -webkit-transform: translateX(0); transform: translateX(0); } } @keyframes headShake { 0% { -webkit-transform: translateX(0); transform: translateX(0); } 6.5% { -webkit-transform: translateX(-6px) rotateY(-9deg); transform: translateX(-6px) rotateY(-9deg); } 18.5% { -webkit-transform: translateX(5px) rotateY(7deg); transform: translateX(5px) rotateY(7deg); } 31.5% { -webkit-transform: translateX(-3px) rotateY(-5deg); transform: translateX(-3px) rotateY(-5deg); } 43.5% { -webkit-transform: translateX(2px) rotateY(3deg); transform: translateX(2px) rotateY(3deg); } 50% { -webkit-transform: translateX(0); transform: translateX(0); } } .headShake { -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; -webkit-animation-name: headShake; animation-name: headShake; } @-webkit-keyframes swing { 20% { -webkit-transform: rotate3d(0, 0, 1, 15deg); transform: rotate3d(0, 0, 1, 15deg); } 40% { -webkit-transform: rotate3d(0, 0, 1, -10deg); transform: rotate3d(0, 0, 1, -10deg); } 60% { -webkit-transform: rotate3d(0, 0, 1, 5deg); transform: rotate3d(0, 0, 1, 5deg); } 80% { -webkit-transform: rotate3d(0, 0, 1, -5deg); transform: rotate3d(0, 0, 1, -5deg); } to { -webkit-transform: rotate3d(0, 0, 1, 0deg); transform: rotate3d(0, 0, 1, 0deg); } } @keyframes swing { 20% { -webkit-transform: rotate3d(0, 0, 1, 15deg); transform: rotate3d(0, 0, 1, 15deg); } 40% { -webkit-transform: rotate3d(0, 0, 1, -10deg); transform: rotate3d(0, 0, 1, -10deg); } 60% { -webkit-transform: rotate3d(0, 0, 1, 5deg); transform: rotate3d(0, 0, 1, 5deg); } 80% { -webkit-transform: rotate3d(0, 0, 1, -5deg); transform: rotate3d(0, 0, 1, -5deg); } to { -webkit-transform: rotate3d(0, 0, 1, 0deg); transform: rotate3d(0, 0, 1, 0deg); } } .swing { -webkit-transform-origin: top center; transform-origin: top center; -webkit-animation-name: swing; animation-name: swing; } @-webkit-keyframes tada { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 10%, 20% { -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); } 30%, 50%, 70%, 90% { -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); } 40%, 60%, 80% { -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } @keyframes tada { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 10%, 20% { -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); } 30%, 50%, 70%, 90% { -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); } 40%, 60%, 80% { -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); } to { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } .tada { -webkit-animation-name: tada; animation-name: tada; } /* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ @-webkit-keyframes wobble { from { -webkit-transform: none; transform: none; } 15% { -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); } 30% { -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); } 45% { -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); } 60% { -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); } 75% { -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); } to { -webkit-transform: none; transform: none; } } @keyframes wobble { from { -webkit-transform: none; transform: none; } 15% { -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); } 30% { -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); } 45% { -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); } 60% { -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); } 75% { -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); } to { -webkit-transform: none; transform: none; } } .wobble { -webkit-animation-name: wobble; animation-name: wobble; } @-webkit-keyframes jello { from, 11.1%, to { -webkit-transform: none; transform: none; } 22.2% { -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); transform: skewX(-12.5deg) skewY(-12.5deg); } 33.3% { -webkit-transform: skewX(6.25deg) skewY(6.25deg); transform: skewX(6.25deg) skewY(6.25deg); } 44.4% { -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); transform: skewX(-3.125deg) skewY(-3.125deg); } 55.5% { -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); transform: skewX(1.5625deg) skewY(1.5625deg); } 66.6% { -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); transform: skewX(-0.78125deg) skewY(-0.78125deg); } 77.7% { -webkit-transform: skewX(0.39063deg) skewY(0.39063deg); transform: skewX(0.39063deg) skewY(0.39063deg); } 88.8% { -webkit-transform: skewX(-0.19531deg) skewY(-0.19531deg); transform: skewX(-0.19531deg) skewY(-0.19531deg); } } @keyframes jello { from, 11.1%, to { -webkit-transform: none; transform: none; } 22.2% { -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); transform: skewX(-12.5deg) skewY(-12.5deg); } 33.3% { -webkit-transform: skewX(6.25deg) skewY(6.25deg); transform: skewX(6.25deg) skewY(6.25deg); } 44.4% { -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); transform: skewX(-3.125deg) skewY(-3.125deg); } 55.5% { -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); transform: skewX(1.5625deg) skewY(1.5625deg); } 66.6% { -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); transform: skewX(-0.78125deg) skewY(-0.78125deg); } 77.7% { -webkit-transform: skewX(0.39063deg) skewY(0.39063deg); transform: skewX(0.39063deg) skewY(0.39063deg); } 88.8% { -webkit-transform: skewX(-0.19531deg) skewY(-0.19531deg); transform: skewX(-0.19531deg) skewY(-0.19531deg); } } .jello { -webkit-animation-name: jello; animation-name: jello; -webkit-transform-origin: center; transform-origin: center; } @-webkit-keyframes bounceIn { from, 20%, 40%, 60%, 80%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } 20% { -webkit-transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1); } 40% { -webkit-transform: scale3d(0.9, 0.9, 0.9); transform: scale3d(0.9, 0.9, 0.9); } 60% { opacity: 1; -webkit-transform: scale3d(1.03, 1.03, 1.03); transform: scale3d(1.03, 1.03, 1.03); } 80% { -webkit-transform: scale3d(0.97, 0.97, 0.97); transform: scale3d(0.97, 0.97, 0.97); } to { opacity: 1; -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } @keyframes bounceIn { from, 20%, 40%, 60%, 80%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } 20% { -webkit-transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1); } 40% { -webkit-transform: scale3d(0.9, 0.9, 0.9); transform: scale3d(0.9, 0.9, 0.9); } 60% { opacity: 1; -webkit-transform: scale3d(1.03, 1.03, 1.03); transform: scale3d(1.03, 1.03, 1.03); } 80% { -webkit-transform: scale3d(0.97, 0.97, 0.97); transform: scale3d(0.97, 0.97, 0.97); } to { opacity: 1; -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } .bounceIn { -webkit-animation-name: bounceIn; animation-name: bounceIn; } @-webkit-keyframes bounceInDown { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: translate3d(0, -3000px, 0); transform: translate3d(0, -3000px, 0); } 60% { opacity: 1; -webkit-transform: translate3d(0, 25px, 0); transform: translate3d(0, 25px, 0); } 75% { -webkit-transform: translate3d(0, -10px, 0); transform: translate3d(0, -10px, 0); } 90% { -webkit-transform: translate3d(0, 5px, 0); transform: translate3d(0, 5px, 0); } to { -webkit-transform: none; transform: none; } } @keyframes bounceInDown { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: translate3d(0, -3000px, 0); transform: translate3d(0, -3000px, 0); } 60% { opacity: 1; -webkit-transform: translate3d(0, 25px, 0); transform: translate3d(0, 25px, 0); } 75% { -webkit-transform: translate3d(0, -10px, 0); transform: translate3d(0, -10px, 0); } 90% { -webkit-transform: translate3d(0, 5px, 0); transform: translate3d(0, 5px, 0); } to { -webkit-transform: none; transform: none; } } .bounceInDown { -webkit-animation-name: bounceInDown; animation-name: bounceInDown; } @-webkit-keyframes bounceInLeft { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: translate3d(-3000px, 0, 0); transform: translate3d(-3000px, 0, 0); } 60% { opacity: 1; -webkit-transform: translate3d(25px, 0, 0); transform: translate3d(25px, 0, 0); } 75% { -webkit-transform: translate3d(-10px, 0, 0); transform: translate3d(-10px, 0, 0); } 90% { -webkit-transform: translate3d(5px, 0, 0); transform: translate3d(5px, 0, 0); } to { -webkit-transform: none; transform: none; } } @keyframes bounceInLeft { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; -webkit-transform: translate3d(-3000px, 0, 0); transform: translate3d(-3000px, 0, 0); } 60% { opacity: 1; -webkit-transform: translate3d(25px, 0, 0); transform: translate3d(25px, 0, 0); } 75% { -webkit-transform: translate3d(-10px, 0, 0); transform: translate3d(-10px, 0, 0); } 90% { -webkit-transform: translate3d(5px, 0, 0); transform: translate3d(5px, 0, 0); } to { -webkit-transform: none; transform: none; } } .bounceInLeft { -webkit-animation-name: bounceInLeft; animation-name: bounceInLeft; } @-webkit-keyframes bounceInRight { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; -webkit-transform: translate3d(3000px, 0, 0); transform: translate3d(3000px, 0, 0); } 60% { opacity: 1; -webkit-transform: translate3d(-25px, 0, 0); transform: translate3d(-25px, 0, 0); } 75% { -webkit-transform: translate3d(10px, 0, 0); transform: translate3d(10px, 0, 0); } 90% { -webkit-transform: translate3d(-5px, 0, 0); transform: translate3d(-5px, 0, 0); } to { -webkit-transform: none; transform: none; } } @keyframes bounceInRight { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; -webkit-transform: translate3d(3000px, 0, 0); transform: translate3d(3000px, 0, 0); } 60% { opacity: 1; -webkit-transform: translate3d(-25px, 0, 0); transform: translate3d(-25px, 0, 0); } 75% { -webkit-transform: translate3d(10px, 0, 0); transform: translate3d(10px, 0, 0); } 90% { -webkit-transform: translate3d(-5px, 0, 0); transform: translate3d(-5px, 0, 0); } to { -webkit-transform: none; transform: none; } } .bounceInRight { -webkit-animation-name: bounceInRight; animation-name: bounceInRight; } @-webkit-keyframes bounceInUp { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; -webkit-transform: translate3d(0, 3000px, 0); transform: translate3d(0, 3000px, 0); } 60% { opacity: 1; -webkit-transform: translate3d(0, -20px, 0); transform: translate3d(0, -20px, 0); } 75% { -webkit-transform: translate3d(0, 10px, 0); transform: translate3d(0, 10px, 0); } 90% { -webkit-transform: translate3d(0, -5px, 0); transform: translate3d(0, -5px, 0); } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } @keyframes bounceInUp { from, 60%, 75%, 90%, to { -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } from { opacity: 0; -webkit-transform: translate3d(0, 3000px, 0); transform: translate3d(0, 3000px, 0); } 60% { opacity: 1; -webkit-transform: translate3d(0, -20px, 0); transform: translate3d(0, -20px, 0); } 75% { -webkit-transform: translate3d(0, 10px, 0); transform: translate3d(0, 10px, 0); } 90% { -webkit-transform: translate3d(0, -5px, 0); transform: translate3d(0, -5px, 0); } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .bounceInUp { -webkit-animation-name: bounceInUp; animation-name: bounceInUp; } @-webkit-keyframes bounceOut { 20% { -webkit-transform: scale3d(0.9, 0.9, 0.9); transform: scale3d(0.9, 0.9, 0.9); } 50%, 55% { opacity: 1; -webkit-transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1); } to { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } } @keyframes bounceOut { 20% { -webkit-transform: scale3d(0.9, 0.9, 0.9); transform: scale3d(0.9, 0.9, 0.9); } 50%, 55% { opacity: 1; -webkit-transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1); } to { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } } .bounceOut { -webkit-animation-name: bounceOut; animation-name: bounceOut; } @-webkit-keyframes bounceOutDown { 20% { -webkit-transform: translate3d(0, 10px, 0); transform: translate3d(0, 10px, 0); } 40%, 45% { opacity: 1; -webkit-transform: translate3d(0, -20px, 0); transform: translate3d(0, -20px, 0); } to { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } } @keyframes bounceOutDown { 20% { -webkit-transform: translate3d(0, 10px, 0); transform: translate3d(0, 10px, 0); } 40%, 45% { opacity: 1; -webkit-transform: translate3d(0, -20px, 0); transform: translate3d(0, -20px, 0); } to { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } } .bounceOutDown { -webkit-animation-name: bounceOutDown; animation-name: bounceOutDown; } @-webkit-keyframes bounceOutLeft { 20% { opacity: 1; -webkit-transform: translate3d(20px, 0, 0); transform: translate3d(20px, 0, 0); } to { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } } @keyframes bounceOutLeft { 20% { opacity: 1; -webkit-transform: translate3d(20px, 0, 0); transform: translate3d(20px, 0, 0); } to { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } } .bounceOutLeft { -webkit-animation-name: bounceOutLeft; animation-name: bounceOutLeft; } @-webkit-keyframes bounceOutRight { 20% { opacity: 1; -webkit-transform: translate3d(-20px, 0, 0); transform: translate3d(-20px, 0, 0); } to { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } } @keyframes bounceOutRight { 20% { opacity: 1; -webkit-transform: translate3d(-20px, 0, 0); transform: translate3d(-20px, 0, 0); } to { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } } .bounceOutRight { -webkit-animation-name: bounceOutRight; animation-name: bounceOutRight; } @-webkit-keyframes bounceOutUp { 20% { -webkit-transform: translate3d(0, -10px, 0); transform: translate3d(0, -10px, 0); } 40%, 45% { opacity: 1; -webkit-transform: translate3d(0, 20px, 0); transform: translate3d(0, 20px, 0); } to { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } } @keyframes bounceOutUp { 20% { -webkit-transform: translate3d(0, -10px, 0); transform: translate3d(0, -10px, 0); } 40%, 45% { opacity: 1; -webkit-transform: translate3d(0, 20px, 0); transform: translate3d(0, 20px, 0); } to { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } } .bounceOutUp { -webkit-animation-name: bounceOutUp; animation-name: bounceOutUp; } @-webkit-keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .fadeIn { -webkit-animation-name: fadeIn; animation-name: fadeIn; } @-webkit-keyframes fadeInDown { from { opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInDown { from { opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInDown { -webkit-animation-name: fadeInDown; animation-name: fadeInDown; } @-webkit-keyframes fadeInDownBig { from { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInDownBig { from { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInDownBig { -webkit-animation-name: fadeInDownBig; animation-name: fadeInDownBig; } @-webkit-keyframes fadeInLeft { from { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInLeft { from { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInLeft { -webkit-animation-name: fadeInLeft; animation-name: fadeInLeft; } @-webkit-keyframes fadeInLeftBig { from { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInLeftBig { from { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInLeftBig { -webkit-animation-name: fadeInLeftBig; animation-name: fadeInLeftBig; } @-webkit-keyframes fadeInRight { from { opacity: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInRight { from { opacity: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInRight { -webkit-animation-name: fadeInRight; animation-name: fadeInRight; } @-webkit-keyframes fadeInRightBig { from { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInRightBig { from { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInRightBig { -webkit-animation-name: fadeInRightBig; animation-name: fadeInRightBig; } @-webkit-keyframes fadeInUp { from { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInUp { from { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInUp { -webkit-animation-name: fadeInUp; animation-name: fadeInUp; } @-webkit-keyframes fadeInUpBig { from { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes fadeInUpBig { from { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } to { opacity: 1; -webkit-transform: none; transform: none; } } .fadeInUpBig { -webkit-animation-name: fadeInUpBig; animation-name: fadeInUpBig; } @-webkit-keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } .fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; } @-webkit-keyframes fadeOutDown { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } } @keyframes fadeOutDown { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } } .fadeOutDown { -webkit-animation-name: fadeOutDown; animation-name: fadeOutDown; } @-webkit-keyframes fadeOutDownBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } } @keyframes fadeOutDownBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, 2000px, 0); transform: translate3d(0, 2000px, 0); } } .fadeOutDownBig { -webkit-animation-name: fadeOutDownBig; animation-name: fadeOutDownBig; } @-webkit-keyframes fadeOutLeft { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } } @keyframes fadeOutLeft { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } } .fadeOutLeft { -webkit-animation-name: fadeOutLeft; animation-name: fadeOutLeft; } @-webkit-keyframes fadeOutLeftBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } } @keyframes fadeOutLeftBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(-2000px, 0, 0); transform: translate3d(-2000px, 0, 0); } } .fadeOutLeftBig { -webkit-animation-name: fadeOutLeftBig; animation-name: fadeOutLeftBig; } @-webkit-keyframes fadeOutRight { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } } @keyframes fadeOutRight { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } } .fadeOutRight { -webkit-animation-name: fadeOutRight; animation-name: fadeOutRight; } @-webkit-keyframes fadeOutRightBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } } @keyframes fadeOutRightBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(2000px, 0, 0); transform: translate3d(2000px, 0, 0); } } .fadeOutRightBig { -webkit-animation-name: fadeOutRightBig; animation-name: fadeOutRightBig; } @-webkit-keyframes fadeOutUp { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } } @keyframes fadeOutUp { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } } .fadeOutUp { -webkit-animation-name: fadeOutUp; animation-name: fadeOutUp; } @-webkit-keyframes fadeOutUpBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } } @keyframes fadeOutUpBig { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(0, -2000px, 0); transform: translate3d(0, -2000px, 0); } } .fadeOutUpBig { -webkit-animation-name: fadeOutUpBig; animation-name: fadeOutUpBig; } @-webkit-keyframes flip { from { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); transform: perspective(400px) rotate3d(0, 1, 0, -360deg); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 40% { -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 50% { -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 80% { -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); transform: perspective(400px) scale3d(0.95, 0.95, 0.95); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } to { -webkit-transform: perspective(400px); transform: perspective(400px); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } } @keyframes flip { from { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); transform: perspective(400px) rotate3d(0, 1, 0, -360deg); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 40% { -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 50% { -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 80% { -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); transform: perspective(400px) scale3d(0.95, 0.95, 0.95); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } to { -webkit-transform: perspective(400px); transform: perspective(400px); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } } .animated.flip { -webkit-backface-visibility: visible; backface-visibility: visible; -webkit-animation-name: flip; animation-name: flip; } @-webkit-keyframes flipInX { from { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); transform: perspective(400px) rotate3d(1, 0, 0, 90deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; opacity: 0; } 40% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); transform: perspective(400px) rotate3d(1, 0, 0, -20deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 60% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); transform: perspective(400px) rotate3d(1, 0, 0, 10deg); opacity: 1; } 80% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); transform: perspective(400px) rotate3d(1, 0, 0, -5deg); } to { -webkit-transform: perspective(400px); transform: perspective(400px); } } @keyframes flipInX { from { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); transform: perspective(400px) rotate3d(1, 0, 0, 90deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; opacity: 0; } 40% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); transform: perspective(400px) rotate3d(1, 0, 0, -20deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 60% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); transform: perspective(400px) rotate3d(1, 0, 0, 10deg); opacity: 1; } 80% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); transform: perspective(400px) rotate3d(1, 0, 0, -5deg); } to { -webkit-transform: perspective(400px); transform: perspective(400px); } } .flipInX { -webkit-backface-visibility: visible !important; backface-visibility: visible !important; -webkit-animation-name: flipInX; animation-name: flipInX; } @-webkit-keyframes flipInY { from { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); transform: perspective(400px) rotate3d(0, 1, 0, 90deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; opacity: 0; } 40% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); transform: perspective(400px) rotate3d(0, 1, 0, -20deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 60% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); transform: perspective(400px) rotate3d(0, 1, 0, 10deg); opacity: 1; } 80% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); transform: perspective(400px) rotate3d(0, 1, 0, -5deg); } to { -webkit-transform: perspective(400px); transform: perspective(400px); } } @keyframes flipInY { from { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); transform: perspective(400px) rotate3d(0, 1, 0, 90deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; opacity: 0; } 40% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); transform: perspective(400px) rotate3d(0, 1, 0, -20deg); -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } 60% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); transform: perspective(400px) rotate3d(0, 1, 0, 10deg); opacity: 1; } 80% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); transform: perspective(400px) rotate3d(0, 1, 0, -5deg); } to { -webkit-transform: perspective(400px); transform: perspective(400px); } } .flipInY { -webkit-backface-visibility: visible !important; backface-visibility: visible !important; -webkit-animation-name: flipInY; animation-name: flipInY; } @-webkit-keyframes flipOutX { from { -webkit-transform: perspective(400px); transform: perspective(400px); } 30% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); transform: perspective(400px) rotate3d(1, 0, 0, -20deg); opacity: 1; } to { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); transform: perspective(400px) rotate3d(1, 0, 0, 90deg); opacity: 0; } } @keyframes flipOutX { from { -webkit-transform: perspective(400px); transform: perspective(400px); } 30% { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); transform: perspective(400px) rotate3d(1, 0, 0, -20deg); opacity: 1; } to { -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); transform: perspective(400px) rotate3d(1, 0, 0, 90deg); opacity: 0; } } .flipOutX { -webkit-animation-name: flipOutX; animation-name: flipOutX; -webkit-backface-visibility: visible !important; backface-visibility: visible !important; } @-webkit-keyframes flipOutY { from { -webkit-transform: perspective(400px); transform: perspective(400px); } 30% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); transform: perspective(400px) rotate3d(0, 1, 0, -15deg); opacity: 1; } to { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); transform: perspective(400px) rotate3d(0, 1, 0, 90deg); opacity: 0; } } @keyframes flipOutY { from { -webkit-transform: perspective(400px); transform: perspective(400px); } 30% { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); transform: perspective(400px) rotate3d(0, 1, 0, -15deg); opacity: 1; } to { -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); transform: perspective(400px) rotate3d(0, 1, 0, 90deg); opacity: 0; } } .flipOutY { -webkit-backface-visibility: visible !important; backface-visibility: visible !important; -webkit-animation-name: flipOutY; animation-name: flipOutY; } @-webkit-keyframes lightSpeedIn { from { -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); transform: translate3d(100%, 0, 0) skewX(-30deg); opacity: 0; } 60% { -webkit-transform: skewX(20deg); transform: skewX(20deg); opacity: 1; } 80% { -webkit-transform: skewX(-5deg); transform: skewX(-5deg); opacity: 1; } to { -webkit-transform: none; transform: none; opacity: 1; } } @keyframes lightSpeedIn { from { -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); transform: translate3d(100%, 0, 0) skewX(-30deg); opacity: 0; } 60% { -webkit-transform: skewX(20deg); transform: skewX(20deg); opacity: 1; } 80% { -webkit-transform: skewX(-5deg); transform: skewX(-5deg); opacity: 1; } to { -webkit-transform: none; transform: none; opacity: 1; } } .lightSpeedIn { -webkit-animation-name: lightSpeedIn; animation-name: lightSpeedIn; -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } @-webkit-keyframes lightSpeedOut { from { opacity: 1; } to { -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); transform: translate3d(100%, 0, 0) skewX(30deg); opacity: 0; } } @keyframes lightSpeedOut { from { opacity: 1; } to { -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); transform: translate3d(100%, 0, 0) skewX(30deg); opacity: 0; } } .lightSpeedOut { -webkit-animation-name: lightSpeedOut; animation-name: lightSpeedOut; -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; } @-webkit-keyframes rotateIn { from { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: rotate3d(0, 0, 1, -200deg); transform: rotate3d(0, 0, 1, -200deg); opacity: 0; } to { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: none; transform: none; opacity: 1; } } @keyframes rotateIn { from { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: rotate3d(0, 0, 1, -200deg); transform: rotate3d(0, 0, 1, -200deg); opacity: 0; } to { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: none; transform: none; opacity: 1; } } .rotateIn { -webkit-animation-name: rotateIn; animation-name: rotateIn; } @-webkit-keyframes rotateInDownLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: none; transform: none; opacity: 1; } } @keyframes rotateInDownLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: none; transform: none; opacity: 1; } } .rotateInDownLeft { -webkit-animation-name: rotateInDownLeft; animation-name: rotateInDownLeft; } @-webkit-keyframes rotateInDownRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: none; transform: none; opacity: 1; } } @keyframes rotateInDownRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: none; transform: none; opacity: 1; } } .rotateInDownRight { -webkit-animation-name: rotateInDownRight; animation-name: rotateInDownRight; } @-webkit-keyframes rotateInUpLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: none; transform: none; opacity: 1; } } @keyframes rotateInUpLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: none; transform: none; opacity: 1; } } .rotateInUpLeft { -webkit-animation-name: rotateInUpLeft; animation-name: rotateInUpLeft; } @-webkit-keyframes rotateInUpRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, -90deg); transform: rotate3d(0, 0, 1, -90deg); opacity: 0; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: none; transform: none; opacity: 1; } } @keyframes rotateInUpRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, -90deg); transform: rotate3d(0, 0, 1, -90deg); opacity: 0; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: none; transform: none; opacity: 1; } } .rotateInUpRight { -webkit-animation-name: rotateInUpRight; animation-name: rotateInUpRight; } @-webkit-keyframes rotateOut { from { -webkit-transform-origin: center; transform-origin: center; opacity: 1; } to { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: rotate3d(0, 0, 1, 200deg); transform: rotate3d(0, 0, 1, 200deg); opacity: 0; } } @keyframes rotateOut { from { -webkit-transform-origin: center; transform-origin: center; opacity: 1; } to { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: rotate3d(0, 0, 1, 200deg); transform: rotate3d(0, 0, 1, 200deg); opacity: 0; } } .rotateOut { -webkit-animation-name: rotateOut; animation-name: rotateOut; } @-webkit-keyframes rotateOutDownLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; opacity: 1; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } } @keyframes rotateOutDownLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; opacity: 1; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, 45deg); transform: rotate3d(0, 0, 1, 45deg); opacity: 0; } } .rotateOutDownLeft { -webkit-animation-name: rotateOutDownLeft; animation-name: rotateOutDownLeft; } @-webkit-keyframes rotateOutDownRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; opacity: 1; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } } @keyframes rotateOutDownRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; opacity: 1; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } } .rotateOutDownRight { -webkit-animation-name: rotateOutDownRight; animation-name: rotateOutDownRight; } @-webkit-keyframes rotateOutUpLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; opacity: 1; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } } @keyframes rotateOutUpLeft { from { -webkit-transform-origin: left bottom; transform-origin: left bottom; opacity: 1; } to { -webkit-transform-origin: left bottom; transform-origin: left bottom; -webkit-transform: rotate3d(0, 0, 1, -45deg); transform: rotate3d(0, 0, 1, -45deg); opacity: 0; } } .rotateOutUpLeft { -webkit-animation-name: rotateOutUpLeft; animation-name: rotateOutUpLeft; } @-webkit-keyframes rotateOutUpRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; opacity: 1; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, 90deg); transform: rotate3d(0, 0, 1, 90deg); opacity: 0; } } @keyframes rotateOutUpRight { from { -webkit-transform-origin: right bottom; transform-origin: right bottom; opacity: 1; } to { -webkit-transform-origin: right bottom; transform-origin: right bottom; -webkit-transform: rotate3d(0, 0, 1, 90deg); transform: rotate3d(0, 0, 1, 90deg); opacity: 0; } } .rotateOutUpRight { -webkit-animation-name: rotateOutUpRight; animation-name: rotateOutUpRight; } @-webkit-keyframes hinge { 0% { -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 20%, 60% { -webkit-transform: rotate3d(0, 0, 1, 80deg); transform: rotate3d(0, 0, 1, 80deg); -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 40%, 80% { -webkit-transform: rotate3d(0, 0, 1, 60deg); transform: rotate3d(0, 0, 1, 60deg); -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; opacity: 1; } to { -webkit-transform: translate3d(0, 700px, 0); transform: translate3d(0, 700px, 0); opacity: 0; } } @keyframes hinge { 0% { -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 20%, 60% { -webkit-transform: rotate3d(0, 0, 1, 80deg); transform: rotate3d(0, 0, 1, 80deg); -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 40%, 80% { -webkit-transform: rotate3d(0, 0, 1, 60deg); transform: rotate3d(0, 0, 1, 60deg); -webkit-transform-origin: top left; transform-origin: top left; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; opacity: 1; } to { -webkit-transform: translate3d(0, 700px, 0); transform: translate3d(0, 700px, 0); opacity: 0; } } .hinge { -webkit-animation-name: hinge; animation-name: hinge; } /* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ @-webkit-keyframes rollIn { from { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); } to { opacity: 1; -webkit-transform: none; transform: none; } } @keyframes rollIn { from { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); } to { opacity: 1; -webkit-transform: none; transform: none; } } .rollIn { -webkit-animation-name: rollIn; animation-name: rollIn; } /* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ @-webkit-keyframes rollOut { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); } } @keyframes rollOut { from { opacity: 1; } to { opacity: 0; -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); } } .rollOut { -webkit-animation-name: rollOut; animation-name: rollOut; } @-webkit-keyframes zoomIn { from { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } 50% { opacity: 1; } } @keyframes zoomIn { from { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } 50% { opacity: 1; } } .zoomIn { -webkit-animation-name: zoomIn; animation-name: zoomIn; } @-webkit-keyframes zoomInDown { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomInDown { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomInDown { -webkit-animation-name: zoomInDown; animation-name: zoomInDown; } @-webkit-keyframes zoomInLeft { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomInLeft { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomInLeft { -webkit-animation-name: zoomInLeft; animation-name: zoomInLeft; } @-webkit-keyframes zoomInRight { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomInRight { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomInRight { -webkit-animation-name: zoomInRight; animation-name: zoomInRight; } @-webkit-keyframes zoomInUp { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomInUp { from { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 60% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomInUp { -webkit-animation-name: zoomInUp; animation-name: zoomInUp; } @-webkit-keyframes zoomOut { from { opacity: 1; } 50% { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } to { opacity: 0; } } @keyframes zoomOut { from { opacity: 1; } 50% { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); } to { opacity: 0; } } .zoomOut { -webkit-animation-name: zoomOut; animation-name: zoomOut; } @-webkit-keyframes zoomOutDown { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); -webkit-transform-origin: center bottom; transform-origin: center bottom; -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomOutDown { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); -webkit-transform-origin: center bottom; transform-origin: center bottom; -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomOutDown { -webkit-animation-name: zoomOutDown; animation-name: zoomOutDown; } @-webkit-keyframes zoomOutLeft { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); } to { opacity: 0; -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); transform: scale(0.1) translate3d(-2000px, 0, 0); -webkit-transform-origin: left center; transform-origin: left center; } } @keyframes zoomOutLeft { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); } to { opacity: 0; -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); transform: scale(0.1) translate3d(-2000px, 0, 0); -webkit-transform-origin: left center; transform-origin: left center; } } .zoomOutLeft { -webkit-animation-name: zoomOutLeft; animation-name: zoomOutLeft; } @-webkit-keyframes zoomOutRight { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); } to { opacity: 0; -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); transform: scale(0.1) translate3d(2000px, 0, 0); -webkit-transform-origin: right center; transform-origin: right center; } } @keyframes zoomOutRight { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); } to { opacity: 0; -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); transform: scale(0.1) translate3d(2000px, 0, 0); -webkit-transform-origin: right center; transform-origin: right center; } } .zoomOutRight { -webkit-animation-name: zoomOutRight; animation-name: zoomOutRight; } @-webkit-keyframes zoomOutUp { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); -webkit-transform-origin: center bottom; transform-origin: center bottom; -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } @keyframes zoomOutUp { 40% { opacity: 1; -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); -webkit-transform-origin: center bottom; transform-origin: center bottom; -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .zoomOutUp { -webkit-animation-name: zoomOutUp; animation-name: zoomOutUp; } @-webkit-keyframes slideInDown { from { -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } @keyframes slideInDown { from { -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .slideInDown { -webkit-animation-name: slideInDown; animation-name: slideInDown; } @-webkit-keyframes slideInLeft { from { -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } @keyframes slideInLeft { from { -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .slideInLeft { -webkit-animation-name: slideInLeft; animation-name: slideInLeft; } @-webkit-keyframes slideInRight { from { -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } @keyframes slideInRight { from { -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .slideInRight { -webkit-animation-name: slideInRight; animation-name: slideInRight; } @-webkit-keyframes slideInUp { from { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } @keyframes slideInUp { from { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); visibility: visible; } to { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .slideInUp { -webkit-animation-name: slideInUp; animation-name: slideInUp; } @-webkit-keyframes slideOutDown { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } } @keyframes slideOutDown { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } } .slideOutDown { -webkit-animation-name: slideOutDown; animation-name: slideOutDown; } @-webkit-keyframes slideOutLeft { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } } @keyframes slideOutLeft { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } } .slideOutLeft { -webkit-animation-name: slideOutLeft; animation-name: slideOutLeft; } @-webkit-keyframes slideOutRight { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } } @keyframes slideOutRight { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } } .slideOutRight { -webkit-animation-name: slideOutRight; animation-name: slideOutRight; } @-webkit-keyframes slideOutUp { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } } @keyframes slideOutUp { from { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } to { visibility: hidden; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } } .slideOutUp { -webkit-animation-name: slideOutUp; animation-name: slideOutUp; } @keyframes spin { from { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: rotate3d(0, 0, 1, -360deg); transform: rotate3d(0, 0, 1, -360deg); } to { -webkit-transform-origin: center; transform-origin: center; -webkit-transform: none; transform: none; } } .spin { -webkit-animation-name: spin; animation-name: spin; } ================================================ FILE: client/libs/codemirror-merge/diff-match-patch.js ================================================ var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]}; diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0, b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a}; diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[new diff_match_patch.Diff(DIFF_INSERT,b)];if(!b)return[new diff_match_patch.Diff(DIFF_DELETE,a)];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[new diff_match_patch.Diff(DIFF_INSERT,e.substring(0,g)),new diff_match_patch.Diff(DIFF_EQUAL,f),new diff_match_patch.Diff(DIFF_INSERT,e.substring(g+f.length))],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[new diff_match_patch.Diff(DIFF_DELETE, a),new diff_match_patch.Diff(DIFF_INSERT,b)]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([new diff_match_patch.Diff(DIFF_EQUAL,a)],c)):c&&100c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n= u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[new diff_match_patch.Diff(DIFF_DELETE,a),new diff_match_patch.Diff(DIFF_INSERT,b)]}; diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)}; diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,g=-1,h=d.length;gd?a=a.substring(c-d):c=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null; var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]}; diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,c.substring(0,d))),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,b.substring(0,e))),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE, a[f+1][1]=b.substring(e),f++;f++}f++}}; diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_); return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c- 1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/; diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;fb)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)}; diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case DIFF_INSERT:b[g]=''+l+"";break;case DIFF_DELETE:b[g]=''+l+"";break;case DIFF_EQUAL:b[g]=""+l+""}}return b.join("")}; diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cthis.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h}; diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c}; diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h, g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;le[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs; 0==e.length||e[e.length-1][0]!=DIFF_EQUAL?(e.push(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c}; diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs); g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,k)));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c { const token = tokens[idx]; if (token.markup === '_') { token.tag = 'u'; } return slf.renderToken(tokens, idx, opts); } module.exports = (md) => { md.renderer.rules.em_open = renderEm; md.renderer.rules.em_close = renderEm; } ================================================ FILE: client/libs/modernizr/modernizr.js ================================================ /*! modernizr 3.6.0 (Custom Build) | MIT * * https://modernizr.com/download/?-setclasses !*/ !function(n,e,s){function o(n){var e=r.className,s=Modernizr._config.classPrefix||"";if(c&&(e=e.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+s+"no-js(\\s|$)");e=e.replace(o,"$1"+s+"js$2")}Modernizr._config.enableClasses&&(e+=" "+s+n.join(" "+s),c?r.className.baseVal=e:r.className=e)}function a(n,e){return typeof n===e}function i(){var n,e,s,o,i,l,r;for(var c in f)if(f.hasOwnProperty(c)){if(n=[],e=f[c],e.name&&(n.push(e.name.toLowerCase()),e.options&&e.options.aliases&&e.options.aliases.length))for(s=0;s code[class*="language-"] { background: hsl(30, 20%, 25%); } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; border: .3em solid hsl(30, 20%, 40%); border-radius: .5em; box-shadow: 1px 1px .5em black inset; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .15em .2em .05em; border-radius: .3em; border: .13em solid hsl(30, 20%, 40%); box-shadow: 1px 1px .3em -.1em black inset; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: hsl(30, 20%, 50%); } .token.punctuation { opacity: .7; } .namespace { opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol { color: hsl(350, 40%, 70%); } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: hsl(75, 70%, 60%); } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: hsl(40, 90%, 60%); } .token.atrule, .token.attr-value, .token.keyword { color: hsl(350, 40%, 70%); } .token.regex, .token.important { color: #e90; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } .token.deleted { color: red; } pre.line-numbers { position: relative; padding-left: 3.8em; counter-reset: linenumber; } pre.line-numbers > code { position: relative; white-space: inherit; } .line-numbers .line-numbers-rows { position: absolute; pointer-events: none; top: 0; font-size: 100%; left: -3.8em; width: 3em; /* works for line-numbers below 1000 lines */ letter-spacing: -1px; border-right: 1px solid #999; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .line-numbers-rows > span { pointer-events: none; display: block; counter-increment: linenumber; } .line-numbers-rows > span:before { content: counter(linenumber); color: #999; display: block; padding-right: 0.8em; text-align: right; } ================================================ FILE: client/libs/prism/prism.js ================================================ /* PrismJS 1.11.0 http://prismjs.com/download.html?themes=prism-dark&languages=markup+css+clike+javascript+c+bash+basic+cpp+csharp+arduino+ruby+elixir+fsharp+go+graphql+handlebars+haskell+ini+java+json+kotlin+latex+less+makefile+markdown+matlab+nginx+objectivec+perl+php+powershell+pug+python+typescript+rust+scss+scala+smalltalk+sql+stylus+swift+vbnet+yaml&plugins=line-numbers */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={manual:_self.Prism&&_self.Prism.manual,disableWorkerMessageHandler:_self.Prism&&_self.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof r?new r(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(w instanceof s)){h.lastIndex=0;var _=h.exec(w),P=1;if(!_&&m&&b!=t.length-1){if(h.lastIndex=k,_=h.exec(e),!_)break;for(var A=_.index+(d?_[1].length:0),j=_.index+_[0].length,x=b,O=k,N=t.length;N>x&&(j>O||!t[x].type&&!t[x-1].greedy);++x)O+=t[x].length,A>=O&&(++b,k=O);if(t[b]instanceof s||t[x-1].greedy)continue;P=x-b,w=e.slice(k,O),_.index-=k}if(_){d&&(p=_[1].length);var A=_.index+p,_=_[0].slice(p),j=A+_.length,S=w.slice(0,A),C=w.slice(j),M=[b,P];S&&(++b,k+=S.length,M.push(S));var E=new s(g,f?n.tokenize(_,f):_,y,_,m);if(M.push(E),C&&M.push(C),Array.prototype.splice.apply(t,M),1!=P&&n.matchGrammar(e,t,r,b,k,!0,g),i)break}else if(i)break}}}}},tokenize:function(e,t){var r=[e],a=t.rest;if(a){for(var l in a)t[l]=a[l];delete t.rest}return n.matchGrammar(e,r,t,0,0,!1),r},hooks:{all:{},add:function(e,t){var r=n.hooks.all;r[e]=r[e]||[],r[e].push(t)},run:function(e,t){var r=n.hooks.all[e];if(r&&r.length)for(var a,l=0;a=r[l++];)a(t)}}},r=n.Token=function(e,t,n,r,a){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length,this.greedy=!!a};if(r.stringify=function(e,t,a){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return r.stringify(n,t,e)}).join("");var l={type:e.type,content:r.stringify(e.content,t,a),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:a};if(e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o=Object.keys(l.attributes).map(function(e){return e+'="'+(l.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+l.tag+' class="'+l.classes.join(" ")+'"'+(o?" "+o:"")+">"+l.content+""},!_self.document)return _self.addEventListener?(n.disableWorkerMessageHandler||_self.addEventListener("message",function(e){var t=JSON.parse(e.data),r=t.language,a=t.code,l=t.immediateClose;_self.postMessage(n.highlight(a,n.languages[r],r)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return a&&(n.filename=a.src,n.manual||a.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; Prism.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css",greedy:!0}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(?:true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\d*\.?\d+(?:[Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[[^\]\r\n]+]|\\.|[^\/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"}}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript",greedy:!0}}),Prism.languages.js=Prism.languages.javascript; Prism.languages.c=Prism.languages.extend("clike",{keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,operator:/-[>-]?|\+\+?|!=?|<>?=?|==?|&&?|\|\|?|[~^%?*\/]/,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)[ful]*\b/i}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+(?:[^\r\n\\]|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,alias:"property",inside:{string:{pattern:/(#\s*include\s*)(?:<.+?>|("|')(?:\\?.)+?\2)/,lookbehind:!0},directive:{pattern:/(#\s*)\b(?:define|defined|elif|else|endif|error|ifdef|ifndef|if|import|include|line|pragma|undef|using)\b/,lookbehind:!0,alias:"keyword"}}},constant:/\b(?:__FILE__|__LINE__|__DATE__|__TIME__|__TIMESTAMP__|__func__|EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|stdin|stdout|stderr)\b/}),delete Prism.languages.c["class-name"],delete Prism.languages.c["boolean"]; !function(e){var t={variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b-?(?:0x[\dA-Fa-f]+|\d*\.?\d+(?:[Ee]-?\d+)?)\b/,operator:/--?|-=|\+\+?|\+=|!=?|~|\*\*?|\*=|\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=|\?|:/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\([^)]+\)|`[^`]+`/,inside:{variable:/^\$\(|^`|\)$|`$/}},/\$(?:[\w#?*!@]+|\{[^}]+\})/i]};e.languages.bash={shebang:{pattern:/^#!\s*\/bin\/bash|^#!\s*\/bin\/sh/,alias:"important"},comment:{pattern:/(^|[^"{\\])#.*/,lookbehind:!0},string:[{pattern:/((?:^|[^<])<<\s*)["']?(\w+?)["']?\s*\r?\n(?:[\s\S])*?\r?\n\2/,lookbehind:!0,greedy:!0,inside:t},{pattern:/(["'])(?:\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0,inside:t}],variable:t.variable,"function":{pattern:/(^|[\s;|&])(?:alias|apropos|apt-get|aptitude|aspell|awk|basename|bash|bc|bg|builtin|bzip2|cal|cat|cd|cfdisk|chgrp|chmod|chown|chroot|chkconfig|cksum|clear|cmp|comm|command|cp|cron|crontab|csplit|cut|date|dc|dd|ddrescue|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|enable|env|ethtool|eval|exec|expand|expect|export|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|getopts|git|grep|groupadd|groupdel|groupmod|groups|gzip|hash|head|help|hg|history|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|jobs|join|kill|killall|less|link|ln|locate|logname|logout|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|make|man|mkdir|mkfifo|mkisofs|mknod|more|most|mount|mtools|mtr|mv|mmv|nano|netstat|nice|nl|nohup|notify-send|npm|nslookup|open|op|passwd|paste|pathchk|ping|pkill|popd|pr|printcap|printenv|printf|ps|pushd|pv|pwd|quota|quotacheck|quotactl|ram|rar|rcp|read|readarray|readonly|reboot|rename|renice|remsync|rev|rm|rmdir|rsync|screen|scp|sdiff|sed|seq|service|sftp|shift|shopt|shutdown|sleep|slocate|sort|source|split|ssh|stat|strace|su|sudo|sum|suspend|sync|tail|tar|tee|test|time|timeout|times|touch|top|traceroute|trap|tr|tsort|tty|type|ulimit|umask|umount|unalias|uname|unexpand|uniq|units|unrar|unshar|uptime|useradd|userdel|usermod|users|uuencode|uudecode|v|vdir|vi|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yes|zip)(?=$|[\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&])(?:let|:|\.|if|then|else|elif|fi|for|break|continue|while|in|case|function|select|do|done|until|echo|exit|return|set|declare)(?=$|[\s;|&])/,lookbehind:!0},"boolean":{pattern:/(^|[\s;|&])(?:true|false)(?=$|[\s;|&])/,lookbehind:!0},operator:/&&?|\|\|?|==?|!=?|<<>|<=?|>=?|=~/,punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];]/};var a=t.variable[1].inside;a["function"]=e.languages.bash["function"],a.keyword=e.languages.bash.keyword,a.boolean=e.languages.bash.boolean,a.operator=e.languages.bash.operator,a.punctuation=e.languages.bash.punctuation}(Prism); Prism.languages.basic={comment:{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},string:{pattern:/"(?:""|[!#$%&'()*,\/:;<=>?^_ +\-.A-Z\d])*"/i,greedy:!0},number:/(?:\b|\B[.-])(?:\d+\.?\d*)(?:E[+-]?\d+)?/i,keyword:/\b(?:AS|BEEP|BLOAD|BSAVE|CALL(?: ABSOLUTE)?|CASE|CHAIN|CHDIR|CLEAR|CLOSE|CLS|COM|COMMON|CONST|DATA|DECLARE|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DIM|DO|DOUBLE|ELSE|ELSEIF|END|ENVIRON|ERASE|ERROR|EXIT|FIELD|FILES|FOR|FUNCTION|GET|GOSUB|GOTO|IF|INPUT|INTEGER|IOCTL|KEY|KILL|LINE INPUT|LOCATE|LOCK|LONG|LOOP|LSET|MKDIR|NAME|NEXT|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPEN|OPTION BASE|OUT|POKE|PUT|READ|REDIM|REM|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SHARED|SINGLE|SELECT CASE|SHELL|SLEEP|STATIC|STEP|STOP|STRING|SUB|SWAP|SYSTEM|THEN|TIMER|TO|TROFF|TRON|TYPE|UNLOCK|UNTIL|USING|VIEW PRINT|WAIT|WEND|WHILE|WRITE)(?:\$|\b)/i,"function":/\b(?:ABS|ACCESS|ACOS|ANGLE|AREA|ARITHMETIC|ARRAY|ASIN|ASK|AT|ATN|BASE|BEGIN|BREAK|CAUSE|CEIL|CHR|CLIP|COLLATE|COLOR|CON|COS|COSH|COT|CSC|DATE|DATUM|DEBUG|DECIMAL|DEF|DEG|DEGREES|DELETE|DET|DEVICE|DISPLAY|DOT|ELAPSED|EPS|ERASABLE|EXLINE|EXP|EXTERNAL|EXTYPE|FILETYPE|FIXED|FP|GO|GRAPH|HANDLER|IDN|IMAGE|IN|INT|INTERNAL|IP|IS|KEYED|LBOUND|LCASE|LEFT|LEN|LENGTH|LET|LINE|LINES|LOG|LOG10|LOG2|LTRIM|MARGIN|MAT|MAX|MAXNUM|MID|MIN|MISSING|MOD|NATIVE|NUL|NUMERIC|OF|OPTION|ORD|ORGANIZATION|OUTIN|OUTPUT|PI|POINT|POINTER|POINTS|POS|PRINT|PROGRAM|PROMPT|RAD|RADIANS|RANDOMIZE|RECORD|RECSIZE|RECTYPE|RELATIVE|REMAINDER|REPEAT|REST|RETRY|REWRITE|RIGHT|RND|ROUND|RTRIM|SAME|SEC|SELECT|SEQUENTIAL|SET|SETTER|SGN|SIN|SINH|SIZE|SKIP|SQR|STANDARD|STATUS|STR|STREAM|STYLE|TAB|TAN|TANH|TEMPLATE|TEXT|THERE|TIME|TIMEOUT|TRACE|TRANSFORM|TRUNCATE|UBOUND|UCASE|USE|VAL|VARIABLE|VIEWPORT|WHEN|WINDOW|WITH|ZER|ZONEWIDTH)(?:\$|\b)/i,operator:/<[=>]?|>=?|[+\-*\/^=&]|\b(?:AND|EQV|IMP|NOT|OR|XOR)\b/i,punctuation:/[,;:()]/}; Prism.languages.cpp=Prism.languages.extend("c",{keyword:/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,"boolean":/\b(?:true|false)\b/,operator:/--?|\+\+?|!=?|<{1,2}=?|>{1,2}=?|->|:{1,2}|={1,2}|\^|~|%|&{1,2}|\|\|?|\?|\*|\/|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/}),Prism.languages.insertBefore("cpp","keyword",{"class-name":{pattern:/(class\s+)\w+/i,lookbehind:!0}}),Prism.languages.insertBefore("cpp","string",{"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}); Prism.languages.csharp=Prism.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[{pattern:/@("|')(?:\1\1|\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0},{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*?\1/,greedy:!0}],number:/\b-?(?:0x[\da-f]+|\d*\.?\d+f?)\b/i}),Prism.languages.insertBefore("csharp","keyword",{"generic-method":{pattern:/[a-z0-9_]+\s*<[^>\r\n]+?>\s*(?=\()/i,alias:"function",inside:{keyword:Prism.languages.csharp.keyword,punctuation:/[<>(),.:]/}},preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(\s*#)\b(?:define|elif|else|endif|endregion|error|if|line|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}}); Prism.languages.arduino=Prism.languages.extend("cpp",{keyword:/\b(?:setup|if|else|while|do|for|return|in|instanceof|default|function|loop|goto|switch|case|new|try|throw|catch|finally|null|break|continue|boolean|bool|void|byte|word|string|String|array|int|long|integer|double)\b/,builtin:/\b(?:KeyboardController|MouseController|SoftwareSerial|EthernetServer|EthernetClient|LiquidCrystal|LiquidCrystal_I2C|RobotControl|GSMVoiceCall|EthernetUDP|EsploraTFT|HttpClient|RobotMotor|WiFiClient|GSMScanner|FileSystem|Scheduler|GSMServer|YunClient|YunServer|IPAddress|GSMClient|GSMModem|Keyboard|Ethernet|Console|GSMBand|Esplora|Stepper|Process|WiFiUDP|GSM_SMS|Mailbox|USBHost|Firmata|PImage|Client|Server|GSMPIN|FileIO|Bridge|Serial|EEPROM|Stream|Mouse|Audio|Servo|File|Task|GPRS|WiFi|Wire|TFT|GSM|SPI|SD|runShellCommandAsynchronously|analogWriteResolution|retrieveCallingNumber|printFirmwareVersion|analogReadResolution|sendDigitalPortPair|noListenOnLocalhost|readJoystickButton|setFirmwareVersion|readJoystickSwitch|scrollDisplayRight|getVoiceCallStatus|scrollDisplayLeft|writeMicroseconds|delayMicroseconds|beginTransmission|getSignalStrength|runAsynchronously|getAsynchronously|listenOnLocalhost|getCurrentCarrier|readAccelerometer|messageAvailable|sendDigitalPorts|lineFollowConfig|countryNameWrite|runShellCommand|readStringUntil|rewindDirectory|readTemperature|setClockDivider|readLightSensor|endTransmission|analogReference|detachInterrupt|countryNameRead|attachInterrupt|encryptionType|readBytesUntil|robotNameWrite|readMicrophone|robotNameRead|cityNameWrite|userNameWrite|readJoystickY|readJoystickX|mouseReleased|openNextFile|scanNetworks|noInterrupts|digitalWrite|beginSpeaker|mousePressed|isActionDone|mouseDragged|displayLogos|noAutoscroll|addParameter|remoteNumber|getModifiers|keyboardRead|userNameRead|waitContinue|processInput|parseCommand|printVersion|readNetworks|writeMessage|blinkVersion|cityNameRead|readMessage|setDataMode|parsePacket|isListening|setBitOrder|beginPacket|isDirectory|motorsWrite|drawCompass|digitalRead|clearScreen|serialEvent|rightToLeft|setTextSize|leftToRight|requestFrom|keyReleased|compassRead|analogWrite|interrupts|WiFiServer|disconnect|playMelody|parseFloat|autoscroll|getPINUsed|setPINUsed|setTimeout|sendAnalog|readSlider|analogRead|beginWrite|createChar|motorsStop|keyPressed|tempoWrite|readButton|subnetMask|debugPrint|macAddress|writeGreen|randomSeed|attachGPRS|readString|sendString|remotePort|releaseAll|mouseMoved|background|getXChange|getYChange|answerCall|getResult|voiceCall|endPacket|constrain|getSocket|writeJSON|getButton|available|connected|findUntil|readBytes|exitValue|readGreen|writeBlue|startLoop|IPAddress|isPressed|sendSysex|pauseMode|gatewayIP|setCursor|getOemKey|tuneWrite|noDisplay|loadImage|switchPIN|onRequest|onReceive|changePIN|playFile|noBuffer|parseInt|overflow|checkPIN|knobRead|beginTFT|bitClear|updateIR|bitWrite|position|writeRGB|highByte|writeRed|setSpeed|readBlue|noStroke|remoteIP|transfer|shutdown|hangCall|beginSMS|endWrite|attached|maintain|noCursor|checkReg|checkPUK|shiftOut|isValid|shiftIn|pulseIn|connect|println|localIP|pinMode|getIMEI|display|noBlink|process|getBand|running|beginSD|drawBMP|lowByte|setBand|release|bitRead|prepare|pointTo|readRed|setMode|noFill|remove|listen|stroke|detach|attach|noTone|exists|buffer|height|bitSet|circle|config|cursor|random|IRread|setDNS|endSMS|getKey|micros|millis|begin|print|write|ready|flush|width|isPIN|blink|clear|press|mkdir|rmdir|close|point|yield|image|BSSID|click|delay|read|text|move|peek|beep|rect|line|open|seek|fill|size|turn|stop|home|find|step|tone|sqrt|RSSI|SSID|end|bit|tan|cos|sin|pow|map|abs|max|min|get|run|put)\b/,constant:/\b(?:DIGITAL_MESSAGE|FIRMATA_STRING|ANALOG_MESSAGE|REPORT_DIGITAL|REPORT_ANALOG|INPUT_PULLUP|SET_PIN_MODE|INTERNAL2V56|SYSTEM_RESET|LED_BUILTIN|INTERNAL1V1|SYSEX_START|INTERNAL|EXTERNAL|DEFAULT|OUTPUT|INPUT|HIGH|LOW)\b/}); !function(e){e.languages.ruby=e.languages.extend("clike",{comment:[/#(?!\{[^\r\n]*?\}).*/,/^=begin(?:\r?\n|\r)(?:.*(?:\r?\n|\r))*?=end/m],keyword:/\b(?:alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var n={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:e.util.clone(e.languages.ruby)}};e.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:/:[a-zA-Z_]\w*(?:[?!]|\b)/}),e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|Fixnum|Float|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z]\w*(?:[?!]|\b)/}),e.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,greedy:!0,inside:{interpolation:n}},{pattern:/("|')(?:#\{[^}]+\}|\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{interpolation:n}}]}(Prism); Prism.languages.elixir={comment:{pattern:/(^|[^#])#(?![{#]).*/m,lookbehind:!0},regex:/~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/,string:[{pattern:/~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/,greedy:!0,inside:{}},{pattern:/("""|''')[\s\S]*?\1/,greedy:!0,inside:{}},{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{}}],atom:{pattern:/(^|[^:]):\w+/,lookbehind:!0,alias:"symbol"},"attr-name":/\w+:(?!:)/,capture:{pattern:/(^|[^&])&(?:[^&\s\d()][^\s()]*|(?=\())/,lookbehind:!0,alias:"function"},argument:{pattern:/(^|[^&])&\d+/,lookbehind:!0,alias:"variable"},attribute:{pattern:/@[\S]+/,alias:"variable"},number:/\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i,keyword:/\b(?:after|alias|and|case|catch|cond|def(?:callback|exception|impl|module|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|require|rescue|try|unless|use|when)\b/,"boolean":/\b(?:true|false|nil)\b/,operator:[/\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/,{pattern:/([^<])<(?!<)/,lookbehind:!0},{pattern:/([^>])>(?!>)/,lookbehind:!0}],punctuation:/<<|>>|[.,%\[\]{}()]/},Prism.languages.elixir.string.forEach(function(e){e.inside={interpolation:{pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"},rest:Prism.util.clone(Prism.languages.elixir)}}}}); Prism.languages.fsharp=Prism.languages.extend("clike",{comment:[{pattern:/(^|[^\\])\(\*[\s\S]*?\*\)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],keyword:/\b(?:let|return|use|yield)(?:!\B|\b)|\b(abstract|and|as|assert|base|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|global|if|in|inherit|inline|interface|internal|lazy|match|member|module|mutable|namespace|new|not|null|of|open|or|override|private|public|rec|select|static|struct|then|to|true|try|type|upcast|val|void|when|while|with|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|include|method|mixin|object|parallel|process|protected|pure|sealed|tailcall|trait|virtual|volatile)\b/,string:{pattern:/(?:"""[\s\S]*?"""|@"(?:""|[^"])*"|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1)B?/,greedy:!0},number:[/\b-?0x[\da-fA-F]+(?:un|lf|LF)?\b/,/\b-?0b[01]+(?:y|uy)?\b/,/\b-?(?:\d*\.?\d+|\d+\.)(?:[fFmM]|[eE][+-]?\d+)?\b/,/\b-?\d+(?:y|uy|s|us|l|u|ul|L|UL|I)?\b/]}),Prism.languages.insertBefore("fsharp","keyword",{preprocessor:{pattern:/^[^\r\n\S]*#.*/m,alias:"property",inside:{directive:{pattern:/(\s*#)\b(?:else|endif|if|light|line|nowarn)\b/,lookbehind:!0,alias:"keyword"}}}}); Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(?:bool|byte|complex(?:64|128)|error|float(?:32|64)|rune|string|u?int(?:8|16|32|64)?|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(?:ln)?|real|recover)\b/,"boolean":/\b(?:_|iota|nil|true|false)\b/,operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:{pattern:/(["'`])(\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0}}),delete Prism.languages.go["class-name"]; Prism.languages.graphql={comment:/#.*/,string:{pattern:/"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/,"boolean":/\b(?:true|false)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":/[a-z_]\w*(?=\s*:)/i,keyword:[{pattern:/(fragment\s+(?!on)[a-z_]\w*\s+|\.{3}\s*)on\b/,lookbehind:!0},/\b(?:query|fragment|mutation)\b/],operator:/!|=|\.{3}/,punctuation:/[!(){}\[\]:=,]/}; !function(e){var a=/\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\}/;e.languages.handlebars=e.languages.extend("markup",{handlebars:{pattern:a,inside:{delimiter:{pattern:/^\{\{\{?|\}\}\}?$/i,alias:"punctuation"},string:/(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,number:/\b-?(?:0x[\dA-Fa-f]+|\d*\.?\d+(?:[Ee][+-]?\d+)?)\b/,"boolean":/\b(?:true|false)\b/,block:{pattern:/^(\s*~?\s*)[#\/]\S+?(?=\s*~?\s*$|\s)/i,lookbehind:!0,alias:"keyword"},brackets:{pattern:/\[[^\]]+\]/,inside:{punctuation:/\[|\]/,variable:/[\s\S]+/}},punctuation:/[!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]/,variable:/[^!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~\s]+/}}}),e.languages.insertBefore("handlebars","tag",{"handlebars-comment":{pattern:/\{\{![\s\S]*?\}\}/,alias:["handlebars","comment"]}}),e.hooks.add("before-highlight",function(e){"handlebars"===e.language&&(e.tokenStack=[],e.backupCode=e.code,e.code=e.code.replace(a,function(a){for(var n=e.tokenStack.length;-1!==e.backupCode.indexOf("___HANDLEBARS"+n+"___");)++n;return e.tokenStack[n]=a,"___HANDLEBARS"+n+"___"}))}),e.hooks.add("before-insert",function(e){"handlebars"===e.language&&(e.code=e.backupCode,delete e.backupCode)}),e.hooks.add("after-highlight",function(a){if("handlebars"===a.language){for(var n=0,t=Object.keys(a.tokenStack);n^\\\/])(?:--[^-!#$%*+=?&@|~.:<>^\\\/].*|{-[\s\S]*?-})/m,lookbehind:!0},"char":/'(?:[^\\']|\\(?:[abfnrtv\\"'&]|\^[A-Z@[\]^_]|NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL|\d+|o[0-7]+|x[0-9a-fA-F]+))'/,string:{pattern:/"(?:[^\\"]|\\(?:[abfnrtv\\"'&]|\^[A-Z@[\]^_]|NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL|\d+|o[0-7]+|x[0-9a-fA-F]+)|\\\s+\\)*"/,greedy:!0},keyword:/\b(?:case|class|data|deriving|do|else|if|in|infixl|infixr|instance|let|module|newtype|of|primitive|then|type|where)\b/,import_statement:{pattern:/((?:\r?\n|\r|^)\s*)import\s+(?:qualified\s+)?(?:[A-Z][\w']*)(?:\.[A-Z][\w']*)*(?:\s+as\s+(?:[A-Z][_a-zA-Z0-9']*)(?:\.[A-Z][\w']*)*)?(?:\s+hiding\b)?/m,lookbehind:!0,inside:{keyword:/\b(?:import|qualified|as|hiding)\b/}},builtin:/\b(?:abs|acos|acosh|all|and|any|appendFile|approxRational|asTypeOf|asin|asinh|atan|atan2|atanh|basicIORun|break|catch|ceiling|chr|compare|concat|concatMap|const|cos|cosh|curry|cycle|decodeFloat|denominator|digitToInt|div|divMod|drop|dropWhile|either|elem|encodeFloat|enumFrom|enumFromThen|enumFromThenTo|enumFromTo|error|even|exp|exponent|fail|filter|flip|floatDigits|floatRadix|floatRange|floor|fmap|foldl|foldl1|foldr|foldr1|fromDouble|fromEnum|fromInt|fromInteger|fromIntegral|fromRational|fst|gcd|getChar|getContents|getLine|group|head|id|inRange|index|init|intToDigit|interact|ioError|isAlpha|isAlphaNum|isAscii|isControl|isDenormalized|isDigit|isHexDigit|isIEEE|isInfinite|isLower|isNaN|isNegativeZero|isOctDigit|isPrint|isSpace|isUpper|iterate|last|lcm|length|lex|lexDigits|lexLitChar|lines|log|logBase|lookup|map|mapM|mapM_|max|maxBound|maximum|maybe|min|minBound|minimum|mod|negate|not|notElem|null|numerator|odd|or|ord|otherwise|pack|pi|pred|primExitWith|print|product|properFraction|putChar|putStr|putStrLn|quot|quotRem|range|rangeSize|read|readDec|readFile|readFloat|readHex|readIO|readInt|readList|readLitChar|readLn|readOct|readParen|readSigned|reads|readsPrec|realToFrac|recip|rem|repeat|replicate|return|reverse|round|scaleFloat|scanl|scanl1|scanr|scanr1|seq|sequence|sequence_|show|showChar|showInt|showList|showLitChar|showParen|showSigned|showString|shows|showsPrec|significand|signum|sin|sinh|snd|sort|span|splitAt|sqrt|subtract|succ|sum|tail|take|takeWhile|tan|tanh|threadToIOResult|toEnum|toInt|toInteger|toLower|toRational|toUpper|truncate|uncurry|undefined|unlines|until|unwords|unzip|unzip3|userError|words|writeFile|zip|zip3|zipWith|zipWith3)\b/,number:/\b(?:\d+(?:\.\d+)?(?:e[+-]?\d+)?|0o[0-7]+|0x[0-9a-f]+)\b/i,operator:/\s\.\s|[-!#$%*+=?&@|~.:<>^\\\/]*\.[-!#$%*+=?&@|~.:<>^\\\/]+|[-!#$%*+=?&@|~.:<>^\\\/]+\.[-!#$%*+=?&@|~.:<>^\\\/]*|[-!#$%*+=?&@|~:<>^\\\/]+|`([A-Z][\w']*\.)*[_a-z][\w']*`/,hvariable:/\b(?:[A-Z][\w']*\.)*[_a-z][\w']*\b/,constant:/\b(?:[A-Z][\w']*\.)*[A-Z][\w']*\b/,punctuation:/[{}[\];(),.:]/}; Prism.languages.ini={comment:/^[ \t]*;.*$/m,selector:/^[ \t]*\[.*?\]/m,constant:/^[ \t]*[^\s=]+?(?=[ \t]*=)/m,"attr-value":{pattern:/=.*/,inside:{punctuation:/^[=]/}}}; Prism.languages.java=Prism.languages.extend("clike",{keyword:/\b(?:abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),Prism.languages.insertBefore("java","function",{annotation:{alias:"punctuation",pattern:/(^|[^.])@\w+/,lookbehind:!0}}); Prism.languages.json={property:/"(?:\\.|[^\\"\r\n])*"(?=\s*:)/i,string:{pattern:/"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,greedy:!0},number:/\b-?(?:0x[\dA-Fa-f]+|\d*\.?\d+(?:[Ee][+-]?\d+)?)\b/,punctuation:/[{}[\]);,]/,operator:/:/g,"boolean":/\b(?:true|false)\b/i,"null":/\bnull\b/i},Prism.languages.jsonp=Prism.languages.json; !function(n){n.languages.kotlin=n.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|else|enum|final|finally|for|fun|get|if|import|in|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|out|override|package|private|protected|public|reified|return|sealed|set|super|tailrec|this|throw|to|try|val|var|when|where|while)\b/,lookbehind:!0},"function":[/\w+(?=\s*\()/,{pattern:/(\.)\w+(?=\s*\{)/,lookbehind:!0}],number:/\b(?:0[bx][\da-fA-F]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete n.languages.kotlin["class-name"],n.languages.insertBefore("kotlin","string",{"raw-string":{pattern:/("""|''')[\s\S]*?\1/,alias:"string"}}),n.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),n.languages.insertBefore("kotlin","function",{label:{pattern:/\w+@|@\w+/,alias:"symbol"}});var e=[{pattern:/\$\{[^}]+\}/,inside:{delimiter:{pattern:/^\$\{|\}$/,alias:"variable"},rest:n.util.clone(n.languages.kotlin)}},{pattern:/\$\w+/,alias:"variable"}];n.languages.kotlin.string.inside=n.languages.kotlin["raw-string"].inside={interpolation:e}}(Prism); !function(a){var e=/\\(?:[^a-z()[\]]|[a-z*]+)/i,n={"equation-command":{pattern:e,alias:"regex"}};a.languages.latex={comment:/%.*/m,cdata:{pattern:/(\\begin\{((?:verbatim|lstlisting)\*?)\})[\s\S]*?(?=\\end\{\2\})/,lookbehind:!0},equation:[{pattern:/\$(?:\\[\s\S]|[^\\$])*\$|\\\([\s\S]*?\\\)|\\\[[\s\S]*?\\\]/,inside:n,alias:"string"},{pattern:/(\\begin\{((?:equation|math|eqnarray|align|multline|gather)\*?)\})[\s\S]*?(?=\\end\{\2\})/,lookbehind:!0,inside:n,alias:"string"}],keyword:{pattern:/(\\(?:begin|end|ref|cite|label|usepackage|documentclass)(?:\[[^\]]+\])?\{)[^}]+(?=\})/,lookbehind:!0},url:{pattern:/(\\url\{)[^}]+(?=\})/,lookbehind:!0},headline:{pattern:/(\\(?:part|chapter|section|subsection|frametitle|subsubsection|paragraph|subparagraph|subsubparagraph|subsubsubparagraph)\*?(?:\[[^\]]+\])?\{)[^}]+(?=\}(?:\[[^\]]+\])?)/,lookbehind:!0,alias:"class-name"},"function":{pattern:e,alias:"selector"},punctuation:/[[\]{}&]/}}(Prism); Prism.languages.less=Prism.languages.extend("css",{comment:[/\/\*[\s\S]*?\*\//,{pattern:/(^|[^\\])\/\/.*/,lookbehind:!0}],atrule:{pattern:/@[\w-]+?(?:\([^{}]+\)|[^(){};])*?(?=\s*\{)/i,inside:{punctuation:/[:()]/}},selector:{pattern:/(?:@\{[\w-]+\}|[^{};\s@])(?:@\{[\w-]+\}|\([^{}]*\)|[^{};@])*?(?=\s*\{)/,inside:{variable:/@+[\w-]+/}},property:/(?:@\{[\w-]+\}|[\w-])+(?:\+_?)?(?=\s*:)/i,punctuation:/[{}();:,]/,operator:/[+\-*\/]/}),Prism.languages.insertBefore("less","punctuation",{"function":Prism.languages.less.function}),Prism.languages.insertBefore("less","property",{variable:[{pattern:/@[\w-]+\s*:/,inside:{punctuation:/:/}},/@@?[\w-]+/],"mixin-usage":{pattern:/([{;]\s*)[.#](?!\d)[\w-]+.*?(?=[(;])/,lookbehind:!0,alias:"function"}}); Prism.languages.makefile={comment:{pattern:/(^|[^\\])#(?:\\(?:\r\n|[\s\S])|[^\\\r\n])*/,lookbehind:!0},string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},builtin:/\.[A-Z][^:#=\s]+(?=\s*:(?!=))/,symbol:{pattern:/^[^:=\r\n]+(?=\s*:(?!=))/m,inside:{variable:/\$+(?:[^(){}:#=\s]+|(?=[({]))/}},variable:/\$+(?:[^(){}:#=\s]+|\([@*%<^+?][DF]\)|(?=[({]))/,keyword:[/-include\b|\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\b/,{pattern:/(\()(?:addsuffix|abspath|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:s|list)?)(?=[ \t])/,lookbehind:!0}],operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/}; Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); Prism.languages.matlab={comment:[/%\{[\s\S]*?\}%/,/%.+/],string:{pattern:/\B'(?:''|[^'\r\n])*'/,greedy:!0},number:/\b-?(?:\d*\.?\d+(?:[eE][+-]?\d+)?(?:[ij])?|[ij])\b/,keyword:/\b(?:break|case|catch|continue|else|elseif|end|for|function|if|inf|NaN|otherwise|parfor|pause|pi|return|switch|try|while)\b/,"function":/(?!\d)\w+(?=\s*\()/,operator:/\.?[*^\/\\']|[+\-:@]|[<>=~]=?|&&?|\|\|?/,punctuation:/\.{3}|[.,;\[\](){}!]/}; Prism.languages.nginx=Prism.languages.extend("clike",{comment:{pattern:/(^|[^"{\\])#.*/,lookbehind:!0},keyword:/\b(?:CONTENT_|DOCUMENT_|GATEWAY_|HTTP_|HTTPS|if_not_empty|PATH_|QUERY_|REDIRECT_|REMOTE_|REQUEST_|SCGI|SCRIPT_|SERVER_|http|events|accept_mutex|accept_mutex_delay|access_log|add_after_body|add_before_body|add_header|addition_types|aio|alias|allow|ancient_browser|ancient_browser_value|auth|auth_basic|auth_basic_user_file|auth_http|auth_http_header|auth_http_timeout|autoindex|autoindex_exact_size|autoindex_localtime|break|charset|charset_map|charset_types|chunked_transfer_encoding|client_body_buffer_size|client_body_in_file_only|client_body_in_single_buffer|client_body_temp_path|client_body_timeout|client_header_buffer_size|client_header_timeout|client_max_body_size|connection_pool_size|create_full_put_path|daemon|dav_access|dav_methods|debug_connection|debug_points|default_type|deny|devpoll_changes|devpoll_events|directio|directio_alignment|disable_symlinks|empty_gif|env|epoll_events|error_log|error_page|expires|fastcgi_buffer_size|fastcgi_buffers|fastcgi_busy_buffers_size|fastcgi_cache|fastcgi_cache_bypass|fastcgi_cache_key|fastcgi_cache_lock|fastcgi_cache_lock_timeout|fastcgi_cache_methods|fastcgi_cache_min_uses|fastcgi_cache_path|fastcgi_cache_purge|fastcgi_cache_use_stale|fastcgi_cache_valid|fastcgi_connect_timeout|fastcgi_hide_header|fastcgi_ignore_client_abort|fastcgi_ignore_headers|fastcgi_index|fastcgi_intercept_errors|fastcgi_keep_conn|fastcgi_max_temp_file_size|fastcgi_next_upstream|fastcgi_no_cache|fastcgi_param|fastcgi_pass|fastcgi_pass_header|fastcgi_read_timeout|fastcgi_redirect_errors|fastcgi_send_timeout|fastcgi_split_path_info|fastcgi_store|fastcgi_store_access|fastcgi_temp_file_write_size|fastcgi_temp_path|flv|geo|geoip_city|geoip_country|google_perftools_profiles|gzip|gzip_buffers|gzip_comp_level|gzip_disable|gzip_http_version|gzip_min_length|gzip_proxied|gzip_static|gzip_types|gzip_vary|if|if_modified_since|ignore_invalid_headers|image_filter|image_filter_buffer|image_filter_jpeg_quality|image_filter_sharpen|image_filter_transparency|imap_capabilities|imap_client_buffer|include|index|internal|ip_hash|keepalive|keepalive_disable|keepalive_requests|keepalive_timeout|kqueue_changes|kqueue_events|large_client_header_buffers|limit_conn|limit_conn_log_level|limit_conn_zone|limit_except|limit_rate|limit_rate_after|limit_req|limit_req_log_level|limit_req_zone|limit_zone|lingering_close|lingering_time|lingering_timeout|listen|location|lock_file|log_format|log_format_combined|log_not_found|log_subrequest|map|map_hash_bucket_size|map_hash_max_size|master_process|max_ranges|memcached_buffer_size|memcached_connect_timeout|memcached_next_upstream|memcached_pass|memcached_read_timeout|memcached_send_timeout|merge_slashes|min_delete_depth|modern_browser|modern_browser_value|mp4|mp4_buffer_size|mp4_max_buffer_size|msie_padding|msie_refresh|multi_accept|open_file_cache|open_file_cache_errors|open_file_cache_min_uses|open_file_cache_valid|open_log_file_cache|optimize_server_names|override_charset|pcre_jit|perl|perl_modules|perl_require|perl_set|pid|pop3_auth|pop3_capabilities|port_in_redirect|post_action|postpone_output|protocol|proxy|proxy_buffer|proxy_buffer_size|proxy_buffering|proxy_buffers|proxy_busy_buffers_size|proxy_cache|proxy_cache_bypass|proxy_cache_key|proxy_cache_lock|proxy_cache_lock_timeout|proxy_cache_methods|proxy_cache_min_uses|proxy_cache_path|proxy_cache_use_stale|proxy_cache_valid|proxy_connect_timeout|proxy_cookie_domain|proxy_cookie_path|proxy_headers_hash_bucket_size|proxy_headers_hash_max_size|proxy_hide_header|proxy_http_version|proxy_ignore_client_abort|proxy_ignore_headers|proxy_intercept_errors|proxy_max_temp_file_size|proxy_method|proxy_next_upstream|proxy_no_cache|proxy_pass|proxy_pass_error_message|proxy_pass_header|proxy_pass_request_body|proxy_pass_request_headers|proxy_read_timeout|proxy_redirect|proxy_redirect_errors|proxy_send_lowat|proxy_send_timeout|proxy_set_body|proxy_set_header|proxy_ssl_session_reuse|proxy_store|proxy_store_access|proxy_temp_file_write_size|proxy_temp_path|proxy_timeout|proxy_upstream_fail_timeout|proxy_upstream_max_fails|random_index|read_ahead|real_ip_header|recursive_error_pages|request_pool_size|reset_timedout_connection|resolver|resolver_timeout|return|rewrite|root|rtsig_overflow_events|rtsig_overflow_test|rtsig_overflow_threshold|rtsig_signo|satisfy|satisfy_any|secure_link_secret|send_lowat|send_timeout|sendfile|sendfile_max_chunk|server|server_name|server_name_in_redirect|server_names_hash_bucket_size|server_names_hash_max_size|server_tokens|set|set_real_ip_from|smtp_auth|smtp_capabilities|so_keepalive|source_charset|split_clients|ssi|ssi_silent_errors|ssi_types|ssi_value_length|ssl|ssl_certificate|ssl_certificate_key|ssl_ciphers|ssl_client_certificate|ssl_crl|ssl_dhparam|ssl_engine|ssl_prefer_server_ciphers|ssl_protocols|ssl_session_cache|ssl_session_timeout|ssl_verify_client|ssl_verify_depth|starttls|stub_status|sub_filter|sub_filter_once|sub_filter_types|tcp_nodelay|tcp_nopush|timeout|timer_resolution|try_files|types|types_hash_bucket_size|types_hash_max_size|underscores_in_headers|uninitialized_variable_warn|upstream|use|user|userid|userid_domain|userid_expires|userid_name|userid_p3p|userid_path|userid_service|valid_referers|variables_hash_bucket_size|variables_hash_max_size|worker_connections|worker_cpu_affinity|worker_priority|worker_processes|worker_rlimit_core|worker_rlimit_nofile|worker_rlimit_sigpending|working_directory|xclient|xml_entities|xslt_entities|xslt_stylesheet|xslt_types)\b/i}),Prism.languages.insertBefore("nginx","keyword",{variable:/\$[a-z_]+/i}); Prism.languages.objectivec=Prism.languages.extend("c",{keyword:/\b(?:asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while|in|self|super)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,string:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|@"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,operator:/-[->]?|\+\+?|!=?|<>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}); Prism.languages.perl={comment:[{pattern:/(^\s*)=\w+[\s\S]*?=cut.*/m,lookbehind:!0},{pattern:/(^|[^\\$])#.*/,lookbehind:!0}],string:[{pattern:/\b(?:q|qq|qx|qw)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s+([a-zA-Z0-9])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\((?:[^()\\]|\\[\s\S])*\)/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\{(?:[^{}\\]|\\[\s\S])*\}/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*\[(?:[^[\]\\]|\\[\s\S])*\]/,greedy:!0},{pattern:/\b(?:q|qq|qx|qw)\s*<(?:[^<>\\]|\\[\s\S])*>/,greedy:!0},{pattern:/("|`)(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0},{pattern:/'(?:[^'\\\r\n]|\\.)*'/,greedy:!0}],regex:[{pattern:/\b(?:m|qr)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s+([a-zA-Z0-9])(?:(?!\1)[^\\]|\\[\s\S])*\1[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\((?:[^()\\]|\\[\s\S])*\)[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\{(?:[^{}\\]|\\[\s\S])*\}[msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*\[(?:[^[\]\\]|\\[\s\S])*\][msixpodualngc]*/,greedy:!0},{pattern:/\b(?:m|qr)\s*<(?:[^<>\\]|\\[\s\S])*>[msixpodualngc]*/,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*([^a-zA-Z0-9\s{(\[<])(?:(?!\2)[^\\]|\\[\s\S])*\2(?:(?!\2)[^\\]|\\[\s\S])*\2[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s+([a-zA-Z0-9])(?:(?!\2)[^\\]|\\[\s\S])*\2(?:(?!\2)[^\\]|\\[\s\S])*\2[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\((?:[^()\\]|\\[\s\S])*\)\s*\((?:[^()\\]|\\[\s\S])*\)[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\{(?:[^{}\\]|\\[\s\S])*\}\s*\{(?:[^{}\\]|\\[\s\S])*\}[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*\[(?:[^[\]\\]|\\[\s\S])*\]\s*\[(?:[^[\]\\]|\\[\s\S])*\][msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/(^|[^-]\b)(?:s|tr|y)\s*<(?:[^<>\\]|\\[\s\S])*>\s*<(?:[^<>\\]|\\[\s\S])*>[msixpodualngcer]*/,lookbehind:!0,greedy:!0},{pattern:/\/(?:[^\/\\\r\n]|\\.)*\/[msixpodualngc]*(?=\s*(?:$|[\r\n,.;})&|\-+*~<>!?^]|(lt|gt|le|ge|eq|ne|cmp|not|and|or|xor|x)\b))/,greedy:!0}],variable:[/[&*$@%]\{\^[A-Z]+\}/,/[&*$@%]\^[A-Z_]/,/[&*$@%]#?(?=\{)/,/[&*$@%]#?(?:(?:::)*'?(?!\d)[\w$]+)+(?:::)*/i,/[&*$@%]\d+/,/(?!%=)[$@%][!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~]/],filehandle:{pattern:/<(?![<=])\S*>|\b_\b/,alias:"symbol"},vstring:{pattern:/v\d+(?:\.\d+)*|\d+(?:\.\d+){2,}/,alias:"string"},"function":{pattern:/sub [a-z0-9_]+/i,inside:{keyword:/sub/}},keyword:/\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|say|state|sub|switch|undef|unless|until|use|when|while)\b/,number:/\b-?(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0b[01](?:_?[01])*|(?:\d(?:_?\d)*)?\.?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)\b/,operator:/-[rwxoRWXOezsfdlpSbctugkTBMAC]\b|\+[+=]?|-[-=>]?|\*\*?=?|\/\/?=?|=[=~>]?|~[~=]?|\|\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\.(?:=|\.\.?)?|[\\?]|\bx(?:=|\b)|\b(?:lt|gt|le|ge|eq|ne|cmp|not|and|or|xor)\b/,punctuation:/[{}[\];(),:]/}; Prism.languages.php=Prism.languages.extend("clike",{string:{pattern:/(["'])(?:\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0},keyword:/\b(?:and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0}}),Prism.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),Prism.languages.insertBefore("php","keyword",{delimiter:{pattern:/\?>|<\?(?:php|=)?/i,alias:"important"},variable:/\$\w+\b/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),Prism.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),Prism.languages.markup&&(Prism.hooks.add("before-highlight",function(e){"php"===e.language&&/(?:<\?php|<\?)/gi.test(e.code)&&(e.tokenStack=[],e.backupCode=e.code,e.code=e.code.replace(/(?:<\?php|<\?)[\s\S]*?(?:\?>|$)/gi,function(a){for(var n=e.tokenStack.length;-1!==e.backupCode.indexOf("___PHP"+n+"___");)++n;return e.tokenStack[n]=a,"___PHP"+n+"___"}),e.grammar=Prism.languages.markup)}),Prism.hooks.add("before-insert",function(e){"php"===e.language&&e.backupCode&&(e.code=e.backupCode,delete e.backupCode)}),Prism.hooks.add("after-highlight",function(e){if("php"===e.language&&e.tokenStack){e.grammar=Prism.languages.php;for(var a=0,n=Object.keys(e.tokenStack);a'+Prism.highlight(r,e.grammar,"php").replace(/\$/g,"$$$$")+"")}e.element.innerHTML=e.highlightedCode}})); Prism.languages.powershell={comment:[{pattern:/(^|[^`])<#[\s\S]*?#>/,lookbehind:!0},{pattern:/(^|[^`])#.*/,lookbehind:!0}],string:[{pattern:/"(?:`[\s\S]|[^`"])*"/,greedy:!0,inside:{"function":{pattern:/[^`]\$\(.*?\)/,inside:{}}}},{pattern:/'(?:[^']|'')*'/,greedy:!0}],namespace:/\[[a-z][\s\S]*?\]/i,"boolean":/\$(?:true|false)\b/i,variable:/\$\w+\b/i,"function":[/\b(?:Add-(?:Computer|Content|History|Member|PSSnapin|Type)|Checkpoint-Computer|Clear-(?:Content|EventLog|History|Item|ItemProperty|Variable)|Compare-Object|Complete-Transaction|Connect-PSSession|ConvertFrom-(?:Csv|Json|StringData)|Convert-Path|ConvertTo-(?:Csv|Html|Json|Xml)|Copy-(?:Item|ItemProperty)|Debug-Process|Disable-(?:ComputerRestore|PSBreakpoint|PSRemoting|PSSessionConfiguration)|Disconnect-PSSession|Enable-(?:ComputerRestore|PSBreakpoint|PSRemoting|PSSessionConfiguration)|Enter-PSSession|Exit-PSSession|Export-(?:Alias|Clixml|Console|Csv|FormatData|ModuleMember|PSSession)|ForEach-Object|Format-(?:Custom|List|Table|Wide)|Get-(?:Alias|ChildItem|Command|ComputerRestorePoint|Content|ControlPanelItem|Culture|Date|Event|EventLog|EventSubscriber|FormatData|Help|History|Host|HotFix|Item|ItemProperty|Job|Location|Member|Module|Process|PSBreakpoint|PSCallStack|PSDrive|PSProvider|PSSession|PSSessionConfiguration|PSSnapin|Random|Service|TraceSource|Transaction|TypeData|UICulture|Unique|Variable|WmiObject)|Group-Object|Import-(?:Alias|Clixml|Csv|LocalizedData|Module|PSSession)|Invoke-(?:Command|Expression|History|Item|RestMethod|WebRequest|WmiMethod)|Join-Path|Limit-EventLog|Measure-(?:Command|Object)|Move-(?:Item|ItemProperty)|New-(?:Alias|Event|EventLog|Item|ItemProperty|Module|ModuleManifest|Object|PSDrive|PSSession|PSSessionConfigurationFile|PSSessionOption|PSTransportOption|Service|TimeSpan|Variable|WebServiceProxy)|Out-(?:Default|File|GridView|Host|Null|Printer|String)|Pop-Location|Push-Location|Read-Host|Receive-(?:Job|PSSession)|Register-(?:EngineEvent|ObjectEvent|PSSessionConfiguration|WmiEvent)|Remove-(?:Computer|Event|EventLog|Item|ItemProperty|Job|Module|PSBreakpoint|PSDrive|PSSession|PSSnapin|TypeData|Variable|WmiObject)|Rename-(?:Computer|Item|ItemProperty)|Reset-ComputerMachinePassword|Resolve-Path|Restart-(?:Computer|Service)|Restore-Computer|Resume-(?:Job|Service)|Save-Help|Select-(?:Object|String|Xml)|Send-MailMessage|Set-(?:Alias|Content|Date|Item|ItemProperty|Location|PSBreakpoint|PSDebug|PSSessionConfiguration|Service|StrictMode|TraceSource|Variable|WmiInstance)|Show-(?:Command|ControlPanelItem|EventLog)|Sort-Object|Split-Path|Start-(?:Job|Process|Service|Sleep|Transaction)|Stop-(?:Computer|Job|Process|Service)|Suspend-(?:Job|Service)|Tee-Object|Test-(?:ComputerSecureChannel|Connection|ModuleManifest|Path|PSSessionConfigurationFile)|Trace-Command|Unblock-File|Undo-Transaction|Unregister-(?:Event|PSSessionConfiguration)|Update-(?:FormatData|Help|List|TypeData)|Use-Transaction|Wait-(?:Event|Job|Process)|Where-Object|Write-(?:Debug|Error|EventLog|Host|Output|Progress|Verbose|Warning))\b/i,/\b(?:ac|cat|chdir|clc|cli|clp|clv|compare|copy|cp|cpi|cpp|cvpa|dbp|del|diff|dir|ebp|echo|epal|epcsv|epsn|erase|fc|fl|ft|fw|gal|gbp|gc|gci|gcs|gdr|gi|gl|gm|gp|gps|group|gsv|gu|gv|gwmi|iex|ii|ipal|ipcsv|ipsn|irm|iwmi|iwr|kill|lp|ls|measure|mi|mount|move|mp|mv|nal|ndr|ni|nv|ogv|popd|ps|pushd|pwd|rbp|rd|rdr|ren|ri|rm|rmdir|rni|rnp|rp|rv|rvpa|rwmi|sal|saps|sasv|sbp|sc|select|set|shcm|si|sl|sleep|sls|sort|sp|spps|spsv|start|sv|swmi|tee|trcm|type|write)\b/i],keyword:/\b(?:Begin|Break|Catch|Class|Continue|Data|Define|Do|DynamicParam|Else|ElseIf|End|Exit|Filter|Finally|For|ForEach|From|Function|If|InlineScript|Parallel|Param|Process|Return|Sequence|Switch|Throw|Trap|Try|Until|Using|Var|While|Workflow)\b/i,operator:{pattern:/(\W?)(?:!|-(eq|ne|gt|ge|lt|le|sh[lr]|not|b?(?:and|x?or)|(?:Not)?(?:Like|Match|Contains|In)|Replace|Join|is(?:Not)?|as)\b|-[-=]?|\+[+=]?|[*\/%]=?)/i,lookbehind:!0},punctuation:/[|{}[\];(),.]/},Prism.languages.powershell.string[0].inside.boolean=Prism.languages.powershell.boolean,Prism.languages.powershell.string[0].inside.variable=Prism.languages.powershell.variable,Prism.languages.powershell.string[0].inside.function.inside=Prism.util.clone(Prism.languages.powershell); !function(e){e.languages.pug={comment:{pattern:/(^([\t ]*))\/\/.*(?:(?:\r?\n|\r)\2[\t ]+.+)*/m,lookbehind:!0},"multiline-script":{pattern:/(^([\t ]*)script\b.*\.[\t ]*)(?:(?:\r?\n|\r(?!\n))(?:\2[\t ]+.+|\s*?(?=\r?\n|\r)))+/m,lookbehind:!0,inside:{rest:e.languages.javascript}},filter:{pattern:/(^([\t ]*)):.+(?:(?:\r?\n|\r(?!\n))(?:\2[\t ]+.+|\s*?(?=\r?\n|\r)))+/m,lookbehind:!0,inside:{"filter-name":{pattern:/^:[\w-]+/,alias:"variable"}}},"multiline-plain-text":{pattern:/(^([\t ]*)[\w\-#.]+\.[\t ]*)(?:(?:\r?\n|\r(?!\n))(?:\2[\t ]+.+|\s*?(?=\r?\n|\r)))+/m,lookbehind:!0},markup:{pattern:/(^[\t ]*)<.+/m,lookbehind:!0,inside:{rest:e.languages.markup}},doctype:{pattern:/((?:^|\n)[\t ]*)doctype(?: .+)?/,lookbehind:!0},"flow-control":{pattern:/(^[\t ]*)(?:if|unless|else|case|when|default|each|while)\b(?: .+)?/m,lookbehind:!0,inside:{each:{pattern:/^each .+? in\b/,inside:{keyword:/\b(?:each|in)\b/,punctuation:/,/}},branch:{pattern:/^(?:if|unless|else|case|when|default|while)\b/,alias:"keyword"},rest:e.languages.javascript}},keyword:{pattern:/(^[\t ]*)(?:block|extends|include|append|prepend)\b.+/m,lookbehind:!0},mixin:[{pattern:/(^[\t ]*)mixin .+/m,lookbehind:!0,inside:{keyword:/^mixin/,"function":/\w+(?=\s*\(|\s*$)/,punctuation:/[(),.]/}},{pattern:/(^[\t ]*)\+.+/m,lookbehind:!0,inside:{name:{pattern:/^\+\w+/,alias:"function"},rest:e.languages.javascript}}],script:{pattern:/(^[\t ]*script(?:(?:&[^(]+)?\([^)]+\))*[\t ]+).+/m,lookbehind:!0,inside:{rest:e.languages.javascript}},"plain-text":{pattern:/(^[\t ]*(?!-)[\w\-#.]*[\w\-](?:(?:&[^(]+)?\([^)]+\))*\/?[\t ]+).+/m,lookbehind:!0},tag:{pattern:/(^[\t ]*)(?!-)[\w\-#.]*[\w\-](?:(?:&[^(]+)?\([^)]+\))*\/?:?/m,lookbehind:!0,inside:{attributes:[{pattern:/&[^(]+\([^)]+\)/,inside:{rest:e.languages.javascript}},{pattern:/\([^)]+\)/,inside:{"attr-value":{pattern:/(=\s*)(?:\{[^}]*\}|[^,)\r\n]+)/,lookbehind:!0,inside:{rest:e.languages.javascript}},"attr-name":/[\w-]+(?=\s*!?=|\s*[,)])/,punctuation:/[!=(),]+/}}],punctuation:/:/}},code:[{pattern:/(^[\t ]*(?:-|!?=)).+/m,lookbehind:!0,inside:{rest:e.languages.javascript}}],punctuation:/[.\-!=|]+/};for(var t="(^([\\t ]*)):{{filter_name}}(?:(?:\\r?\\n|\\r(?!\\n))(?:\\2[\\t ]+.+|\\s*?(?=\\r?\\n|\\r)))+",n=[{filter:"atpl",language:"twig"},{filter:"coffee",language:"coffeescript"},"ejs","handlebars","hogan","less","livescript","markdown","mustache","plates",{filter:"sass",language:"scss"},"stylus","swig"],a={},i=0,r=n.length;r>i;i++){var s=n[i];s="string"==typeof s?{filter:s,language:s}:s,e.languages[s.language]&&(a["filter-"+s.filter]={pattern:RegExp(t.replace("{{filter_name}}",s.filter),"m"),lookbehind:!0,inside:{"filter-name":{pattern:/^:[\w-]+/,alias:"variable"},rest:e.languages[s.language]}})}e.languages.insertBefore("pug","filter",a)}(Prism); Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},"triple-quoted-string":{pattern:/("""|''')[\s\S]+?\1/,greedy:!0,alias:"string"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,"boolean":/\b(?:True|False|None)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/}; Prism.languages.typescript=Prism.languages.extend("javascript",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield|false|true|module|declare|constructor|namespace|abstract|require|type)\b/,builtin:/\b(?:string|Function|any|number|boolean|Array|symbol|console)\b/}),Prism.languages.ts=Prism.languages.typescript; Prism.languages.rust={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:[{pattern:/b?r(#*)"(?:\\.|(?!"\1)[^\\\r\n])*"\1/,greedy:!0},{pattern:/b?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0}],keyword:/\b(?:abstract|alignof|as|be|box|break|const|continue|crate|do|else|enum|extern|false|final|fn|for|if|impl|in|let|loop|match|mod|move|mut|offsetof|once|override|priv|pub|pure|ref|return|sizeof|static|self|struct|super|true|trait|type|typeof|unsafe|unsized|use|virtual|where|while|yield)\b/,attribute:{pattern:/#!?\[.+?\]/,greedy:!0,alias:"attr-name"},"function":[/\w+(?=\s*\()/,/\w+!(?=\s*\(|\[)/],"macro-rules":{pattern:/\w+!/,alias:"function"},number:/\b-?(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(\d(?:_?\d)*)?\.?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:[iu](?:8|16|32|64)?|f32|f64))?\b/,"closure-params":{pattern:/\|[^|]*\|(?=\s*[{-])/,inside:{punctuation:/[|:,]/,operator:/[&*]/}},punctuation:/[{}[\];(),:]|\.+|->/,operator:/[-+*\/%!^=]=?|@|&[&=]?|\|[|=]?|<>?=?/}; Prism.languages.scss=Prism.languages.extend("css",{comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},atrule:{pattern:/@[\w-]+(?:\([^()]+\)|[^(])*?(?=\s+[{;])/,inside:{rule:/@[\w-]+/}},url:/(?:[-a-z]+-)*url(?=\()/i,selector:{pattern:/(?=\S)[^@;{}()]?(?:[^@;{}()]|&|#\{\$[-\w]+\})+(?=\s*\{(?:\}|\s|[^}]+[:{][^}]+))/m,inside:{parent:{pattern:/&/,alias:"important"},placeholder:/%[-\w]+/,variable:/\$[-\w]+|#\{\$[-\w]+\}/}}}),Prism.languages.insertBefore("scss","atrule",{keyword:[/@(?:if|else(?: if)?|for|each|while|import|extend|debug|warn|mixin|include|function|return|content)/i,{pattern:/( +)(?:from|through)(?= )/,lookbehind:!0}]}),Prism.languages.scss.property={pattern:/(?:[\w-]|\$[-\w]+|#\{\$[-\w]+\})+(?=\s*:)/i,inside:{variable:/\$[-\w]+|#\{\$[-\w]+\}/}},Prism.languages.insertBefore("scss","important",{variable:/\$[-\w]+|#\{\$[-\w]+\}/}),Prism.languages.insertBefore("scss","function",{placeholder:{pattern:/%[-\w]+/,alias:"selector"},statement:{pattern:/\B!(?:default|optional)\b/i,alias:"keyword"},"boolean":/\b(?:true|false)\b/,"null":/\bnull\b/,operator:{pattern:/(\s)(?:[-+*\/%]|[=!]=|<=?|>=?|and|or|not)(?=\s)/,lookbehind:!0}}),Prism.languages.scss.atrule.inside.rest=Prism.util.clone(Prism.languages.scss); Prism.languages.scala=Prism.languages.extend("java",{keyword:/<-|=>|\b(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|null|object|override|package|private|protected|return|sealed|self|super|this|throw|trait|try|type|val|var|while|with|yield)\b/,string:[{pattern:/"""[\s\S]*?"""/,greedy:!0},{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0}],builtin:/\b(?:String|Int|Long|Short|Byte|Boolean|Double|Float|Char|Any|AnyRef|AnyVal|Unit|Nothing)\b/,number:/\b(?:0x[\da-f]*\.?[\da-f]+|\d*\.?\d+e?\d*[dfl]?)\b/i,symbol:/'[^\d\s\\]\w*/}),delete Prism.languages.scala["class-name"],delete Prism.languages.scala["function"]; Prism.languages.smalltalk={comment:/"(?:""|[^"])+"/,string:/'(?:''|[^'])+'/,symbol:/#[\da-z]+|#(?:-|([+\/\\*~<>=@%|&?!])\1?)|#(?=\()/i,"block-arguments":{pattern:/(\[\s*):[^\[|]*\|/,lookbehind:!0,inside:{variable:/:[\da-z]+/i,punctuation:/\|/}},"temporary-variables":{pattern:/\|[^|]+\|/,inside:{variable:/[\da-z]+/i,punctuation:/\|/}},keyword:/\b(?:nil|true|false|self|super|new)\b/,character:{pattern:/\$./,alias:"string"},number:[/\d+r-?[\dA-Z]+(?:\.[\dA-Z]+)?(?:e-?\d+)?/,/(?:\B-|\b)\d+(?:\.\d+)?(?:e-?\d+)?/],operator:/[<=]=?|:=|~[~=]|\/\/?|\\\\|>[>=]?|[!^+\-*&|,@]/,punctuation:/[.;:?\[\](){}]/}; Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\])*\2/,greedy:!0,lookbehind:!0},variable:/@[\w.$]+|@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,"function":/\b(?:COUNT|SUM|AVG|MIN|MAX|FIRST|LAST|UCASE|LCASE|MID|LEN|ROUND|NOW|FORMAT)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR VARYING|CHARACTER (?:SET|VARYING)|CHARSET|CHECK|CHECKPOINT|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMN|COLUMNS|COMMENT|COMMIT|COMMITTED|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS|CONTAINSTABLE|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|DATA(?:BASES?)?|DATE(?:TIME)?|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITER(?:S)?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE(?: PRECISION)?|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE KEY|ELSE|ENABLE|ENCLOSED BY|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPE(?:D BY)?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|IDENTITY(?:_INSERT|COL)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTO|INVOKER|ISOLATION LEVEL|JOIN|KEYS?|KILL|LANGUAGE SQL|LAST|LEFT|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MODIFIES SQL DATA|MODIFY|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL(?: CHAR VARYING| CHARACTER(?: VARYING)?| VARCHAR)?|NATURAL|NCHAR(?: VARCHAR)?|NEXT|NO(?: SQL|CHECK|CYCLE)?|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READ(?:S SQL DATA|TEXT)?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEATABLE|REPLICATION|REQUIRE|RESTORE|RESTRICT|RETURNS?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE MODE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|START(?:ING BY)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED BY|TEXT(?:SIZE)?|THEN|TIMESTAMP|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNPIVOT|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?)\b/i,"boolean":/\b(?:TRUE|FALSE|NULL)\b/i,number:/\b-?(?:0x)?\d*\.?[\da-f]+\b/,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|IN|LIKE|NOT|OR|IS|DIV|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/}; !function(n){var t={url:/url\((["']?).*?\1\)/i,string:{pattern:/("|')(?:(?!\1)[^\\\r\n]|\\(?:\r\n|[\s\S]))*\1/,greedy:!0},interpolation:null,func:null,important:/\B!(?:important|optional)\b/i,keyword:{pattern:/(^|\s+)(?:(?:if|else|for|return|unless)(?=\s+|$)|@[\w-]+)/,lookbehind:!0},hexcode:/#[\da-f]{3,6}/i,number:/\b\d+(?:\.\d+)?%?/,"boolean":/\b(?:true|false)\b/,operator:[/~|[+!\/%<>?=]=?|[-:]=|\*[*=]?|\.+|&&|\|\||\B-\B|\b(?:and|in|is(?: a| defined| not|nt)?|not|or)\b/],punctuation:/[{}()\[\];:,]/};t.interpolation={pattern:/\{[^\r\n}:]+\}/,alias:"variable",inside:n.util.clone(t)},t.func={pattern:/[\w-]+\([^)]*\).*/,inside:{"function":/^[^(]+/,rest:n.util.clone(t)}},n.languages.stylus={comment:{pattern:/(^|[^\\])(\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},"atrule-declaration":{pattern:/(^\s*)@.+/m,lookbehind:!0,inside:{atrule:/^@[\w-]+/,rest:t}},"variable-declaration":{pattern:/(^[ \t]*)[\w$-]+\s*.?=[ \t]*(?:(?:\{[^}]*\}|.+)|$)/m,lookbehind:!0,inside:{variable:/^\S+/,rest:t}},statement:{pattern:/(^[ \t]*)(?:if|else|for|return|unless)[ \t]+.+/m,lookbehind:!0,inside:{keyword:/^\S+/,rest:t}},"property-declaration":{pattern:/((?:^|\{)([ \t]*))(?:[\w-]|\{[^}\r\n]+\})+(?:\s*:\s*|[ \t]+)[^{\r\n]*(?:;|[^{\r\n,](?=$)(?!(\r?\n|\r)(?:\{|\2[ \t]+)))/m,lookbehind:!0,inside:{property:{pattern:/^[^\s:]+/,inside:{interpolation:t.interpolation}},rest:t}},selector:{pattern:/(^[ \t]*)(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\))?|\{[^}\r\n]+\})+)(?:(?:\r?\n|\r)(?:\1(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\))?|\{[^}\r\n]+\})+)))*(?:,$|\{|(?=(?:\r?\n|\r)(?:\{|\1[ \t]+)))/m,lookbehind:!0,inside:{interpolation:t.interpolation,punctuation:/[{},]/}},func:t.func,string:t.string,interpolation:t.interpolation,punctuation:/[{}()\[\];:.]/}}(Prism); Prism.languages.swift=Prism.languages.extend("clike",{string:{pattern:/("|')(\\(?:\((?:[^()]|\([^)]+\))+\)|\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{interpolation:{pattern:/\\\((?:[^()]|\([^)]+\))+\)/,inside:{delimiter:{pattern:/^\\\(|\)$/,alias:"variable"}}}}},keyword:/\b(?:as|associativity|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic(?:Type)?|else|enum|extension|fallthrough|final|for|func|get|guard|if|import|in|infix|init|inout|internal|is|lazy|left|let|mutating|new|none|nonmutating|operator|optional|override|postfix|precedence|prefix|private|Protocol|public|repeat|required|rethrows|return|right|safe|self|Self|set|static|struct|subscript|super|switch|throws?|try|Type|typealias|unowned|unsafe|var|weak|where|while|willSet|__(?:COLUMN__|FILE__|FUNCTION__|LINE__))\b/,number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,constant:/\b(?:nil|[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,atrule:/@\b(?:IB(?:Outlet|Designable|Action|Inspectable)|class_protocol|exported|noreturn|NS(?:Copying|Managed)|objc|UIApplicationMain|auto_closure)\b/,builtin:/\b(?:[A-Z]\S+|abs|advance|alignof(?:Value)?|assert|contains|count(?:Elements)?|debugPrint(?:ln)?|distance|drop(?:First|Last)|dump|enumerate|equal|filter|find|first|getVaList|indices|isEmpty|join|last|lexicographicalCompare|map|max(?:Element)?|min(?:Element)?|numericCast|overlaps|partition|print(?:ln)?|reduce|reflect|reverse|sizeof(?:Value)?|sort(?:ed)?|split|startsWith|stride(?:of(?:Value)?)?|suffix|swap|toDebugString|toString|transcode|underestimateCount|unsafeBitCast|with(?:ExtendedLifetime|Unsafe(?:MutablePointers?|Pointers?)|VaList))\b/}),Prism.languages.swift.string.inside.interpolation.inside.rest=Prism.util.clone(Prism.languages.swift); Prism.languages.vbnet=Prism.languages.extend("basic",{keyword:/(?:\b(?:ADDHANDLER|ADDRESSOF|ALIAS|AND|ANDALSO|AS|BEEP|BLOAD|BOOLEAN|BSAVE|BYREF|BYTE|BYVAL|CALL(?: ABSOLUTE)?|CASE|CATCH|CBOOL|CBYTE|CCHAR|CDATE|CDEC|CDBL|CHAIN|CHAR|CHDIR|CINT|CLASS|CLEAR|CLNG|CLOSE|CLS|COBJ|COM|COMMON|CONST|CONTINUE|CSBYTE|CSHORT|CSNG|CSTR|CTYPE|CUINT|CULNG|CUSHORT|DATA|DATE|DECIMAL|DECLARE|DEFAULT|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DELEGATE|DIM|DIRECTCAST|DO|DOUBLE|ELSE|ELSEIF|END|ENUM|ENVIRON|ERASE|ERROR|EVENT|EXIT|FALSE|FIELD|FILES|FINALLY|FOR(?: EACH)?|FRIEND|FUNCTION|GET|GETTYPE|GETXMLNAMESPACE|GLOBAL|GOSUB|GOTO|HANDLES|IF|IMPLEMENTS|IMPORTS|IN|INHERITS|INPUT|INTEGER|INTERFACE|IOCTL|IS|ISNOT|KEY|KILL|LINE INPUT|LET|LIB|LIKE|LOCATE|LOCK|LONG|LOOP|LSET|ME|MKDIR|MOD|MODULE|MUSTINHERIT|MUSTOVERRIDE|MYBASE|MYCLASS|NAME|NAMESPACE|NARROWING|NEW|NEXT|NOT|NOTHING|NOTINHERITABLE|NOTOVERRIDABLE|OBJECT|OF|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPERATOR|OPEN|OPTION(?: BASE)?|OPTIONAL|OR|ORELSE|OUT|OVERLOADS|OVERRIDABLE|OVERRIDES|PARAMARRAY|PARTIAL|POKE|PRIVATE|PROPERTY|PROTECTED|PUBLIC|PUT|RAISEEVENT|READ|READONLY|REDIM|REM|REMOVEHANDLER|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SBYTE|SELECT(?: CASE)?|SET|SHADOWS|SHARED|SHORT|SINGLE|SHELL|SLEEP|STATIC|STEP|STOP|STRING|STRUCTURE|SUB|SYNCLOCK|SWAP|SYSTEM|THEN|THROW|TIMER|TO|TROFF|TRON|TRUE|TRY|TRYCAST|TYPE|TYPEOF|UINTEGER|ULONG|UNLOCK|UNTIL|USHORT|USING|VIEW PRINT|WAIT|WEND|WHEN|WHILE|WIDENING|WITH|WITHEVENTS|WRITE|WRITEONLY|XOR)|\B(?:#CONST|#ELSE|#ELSEIF|#END|#IF))(?:\$|\b)/i,comment:[{pattern:/(?:!|REM\b).+/i,inside:{keyword:/^REM/i}},{pattern:/(^|[^\\:])'.*/,lookbehind:!0}]}); Prism.languages.yaml={scalar:{pattern:/([\-:]\s*(?:![^\s]+)?[ \t]*[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)[^\r\n]+(?:\2[^\r\n]+)*)/,lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:/(\s*(?:^|[:\-,[{\r\n?])[ \t]*(?:![^\s]+)?[ \t]*)[^\r\n{[\]},#\s]+?(?=\s*:\s)/,lookbehind:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?)?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?)(?=[ \t]*(?:$|,|]|}))/m,lookbehind:!0,alias:"number"},"boolean":{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:true|false)[ \t]*(?=$|,|]|})/im,lookbehind:!0,alias:"important"},"null":{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:null|~)[ \t]*(?=$|,|]|})/im,lookbehind:!0,alias:"important"},string:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)("|')(?:(?!\2)[^\\\r\n]|\\.)*\2(?=[ \t]*(?:$|,|]|}))/m,lookbehind:!0,greedy:!0},number:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)[+\-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+\.?\d*|\.?\d+)(?:e[+-]?\d+)?|\.inf|\.nan)[ \t]*(?=$|,|]|})/im,lookbehind:!0},tag:/![^\s]+/,important:/[&*][\w]+/,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./}; !function(){if("undefined"!=typeof self&&self.Prism&&self.document){var e="line-numbers",t=/\n(?!$)/g,n=function(e){var n=r(e),s=n["white-space"];if("pre-wrap"===s||"pre-line"===s){var l=e.querySelector("code"),i=e.querySelector(".line-numbers-rows"),a=e.querySelector(".line-numbers-sizer"),o=l.textContent.split(t);a||(a=document.createElement("span"),a.className="line-numbers-sizer",l.appendChild(a)),a.style.display="block",o.forEach(function(e,t){a.textContent=e||"\n";var n=a.getBoundingClientRect().height;i.children[t].style.height=n+"px"}),a.textContent="",a.style.display="none"}},r=function(e){return e?window.getComputedStyle?getComputedStyle(e):e.currentStyle||null:null};window.addEventListener("resize",function(){Array.prototype.forEach.call(document.querySelectorAll("pre."+e),n)}),Prism.hooks.add("complete",function(e){if(e.code){var r=e.element.parentNode,s=/\s*\bline-numbers\b\s*/;if(r&&/pre/i.test(r.nodeName)&&(s.test(r.className)||s.test(e.element.className))&&!e.element.querySelector(".line-numbers-rows")){s.test(e.element.className)&&(e.element.className=e.element.className.replace(s," ")),s.test(r.className)||(r.className+=" line-numbers");var l,i=e.code.match(t),a=i?i.length+1:1,o=new Array(a+1);o=o.join(""),l=document.createElement("span"),l.setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=o,r.hasAttribute("data-start")&&(r.style.counterReset="linenumber "+(parseInt(r.getAttribute("data-start"),10)-1)),e.element.appendChild(l),n(r),Prism.hooks.run("line-numbers",e)}}}),Prism.hooks.add("line-numbers",function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}),Prism.plugins.lineNumbers={getLine:function(t,n){if("PRE"===t.tagName&&t.classList.contains(e)){var r=t.querySelector(".line-numbers-rows"),s=parseInt(t.getAttribute("data-start"),10)||1,l=s+(r.children.length-1);s>n&&(n=s),n>l&&(n=l);var i=n-s;return r.children[i]}}}}}(); ================================================ FILE: client/libs/twemoji/twemoji-awesome.scss ================================================ .twa { display: inline-block; height: 1em; width: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; background-repeat: no-repeat; background-position: center center; background-size: 1em 1em; } $size-map: ( "lg": 1.33, "2x": 2, "3x": 3, "4x": 4, "5x": 5 ); @each $name, $size in $size-map { .twa-#{$name} { height: 1em * $size; width: 1em * $size; margin: 0 .05em * $size 0 .1em * $size; vertical-align: -0.1em * $size; background-size: 1em * $size 1em * $size; } } $emoji-map: ( "1f604": "smile", "1f606": "laughing", "1f60a": "blush", "1f603": "smiley", "263a": "relaxed", "1f60f": "smirk", "1f60d": "heart-eyes", "1f618": "kissing-heart", "1f61a": "kissing-closed-eyes", "1f633": "flushed", "1f625": "relieved", "1f60c": "satisfied", "1f601": "grin", "1f609": "wink", "1f61c": "stuck-out-tongue-winking-eye", "1f61d": "stuck-out-tongue-closed-eyes", "1f600": "grinning", "1f617": "kissing", "1f619": "kissing-smiling-eyes", "1f61b": "stuck-out-tongue", "1f634": "sleeping", "1f61f": "worried", "1f626": "frowning", "1f627": "anguished", "1f62e": "open-mouth", "1f62c": "grimacing", "1f615": "confused", "1f62f": "hushed", "1f611": "expressionless", "1f612": "unamused", "1f605": "sweat-smile", "1f613": "sweat", "1f629": "weary", "1f614": "pensive", "1f61e": "disappointed", "1f616": "confounded", "1f628": "fearful", "1f630": "cold-sweat", "1f623": "persevere", "1f622": "cry", "1f62d": "sob", "1f602": "joy", "1f632": "astonished", "1f631": "scream", "1f62b": "tired-face", "1f620": "angry", "1f621": "rage", "1f624": "triumph", "1f62a": "sleepy", "1f60b": "yum", "1f637": "mask", "1f60e": "sunglasses", "1f635": "dizzy-face", "1f47f": "imp", "1f608": "smiling-imp", "1f610": "neutral-face", "1f636": "no-mouth", "1f607": "innocent", "1f47d": "alien", "1f49b": "yellow-heart", "1f499": "blue-heart", "1f49c": "purple-heart", "2764": "heart", "1f49a": "green-heart", "1f494": "broken-heart", "1f493": "heartbeat", "1f497": "heartpulse", "1f495": "two-hearts", "1f49e": "revolving-hearts", "1f498": "cupid", "1f496": "sparkling-heart", "2728": "sparkles", "2b50": "star", "1f31f": "star2", "1f4ab": "dizzy", "1f4a5": "boom", "1f4a2": "anger", "2757": "exclamation", "2753": "question", "2755": "grey-exclamation", "2754": "grey-question", "1f4a4": "zzz", "1f4a8": "dash", "1f4a6": "sweat-drops", "1f3b6": "notes", "1f3b5": "musical-note", "1f525": "fire", "1f4a9": "poop", "1f44d": "thumbsup", "1f44e": "thumbsdown", "1f44c": "ok-hand", "1f44a": "punch", "270a": "fist", "270c": "v", "1f44b": "wave", "270b": "hand", "1f450": "open-hands", "261d": "point-up", "1f447": "point-down", "1f448": "point-left", "1f449": "point-right", "1f64c": "raised-hands", "1f64f": "pray", "1f446": "point-up-2", "1f44f": "clap", "1f4aa": "muscle", "1f6b6": "walking", "1f3c3": "runner", "1f46b": "couple", "1f46a": "family", "1f46c": "two-men-holding-hands", "1f46d": "two-women-holding-hands", "1f483": "dancer", "1f46f": "dancers", "1f646": "ok-woman", "1f645": "no-good", "1f481": "information-desk-person", "1f64b": "raised-hand", "1f470": "bride-with-veil", "1f64e": "person-with-pouting-face", "1f64d": "person-frowning", "1f647": "bow", "1f48f": "couplekiss", "1f491": "couple-with-heart", "1f486": "massage", "1f487": "haircut", "1f485": "nail-care", "1f466": "boy", "1f467": "girl", "1f469": "woman", "1f468": "man", "1f476": "baby", "1f475": "older-woman", "1f474": "older-man", "1f471": "person-with-blond-hair", "1f472": "man-with-gua-pi-mao", "1f473": "man-with-turban", "1f477": "construction-worker", "1f46e": "cop", "1f47c": "angel", "1f478": "princess", "1f63a": "smiley-cat", "1f638": "smile-cat", "1f63b": "heart-eyes-cat", "1f63d": "kissing-cat", "1f63c": "smirk-cat", "1f640": "scream-cat", "1f63f": "crying-cat-face", "1f639": "joy-cat", "1f63e": "pouting-cat", "1f479": "japanese-ogre", "1f47a": "japanese-goblin", "1f648": "see-no-evil", "1f649": "hear-no-evil", "1f64a": "speak-no-evil", "1f482": "guardsman", "1f480": "skull", "1f463": "feet", "1f444": "lips", "1f48b": "kiss", "1f4a7": "droplet", "1f442": "ear", "1f440": "eyes", "1f443": "nose", "1f445": "tongue", "1f48c": "love-letter", "1f464": "bust-in-silhouette", "1f465": "busts-in-silhouette", "1f4ac": "speech-balloon", "1f4ad": "thought-balloon", "2600": "sunny", "2614": "umbrella", "2601": "cloud", "2744": "snowflake", "26c4": "snowman", "26a1": "zap", "1f300": "cyclone", "1f301": "foggy", "1f30a": "ocean", "1f431": "cat", "1f436": "dog", "1f42d": "mouse", "1f439": "hamster", "1f430": "rabbit", "1f43a": "wolf", "1f438": "frog", "1f42f": "tiger", "1f428": "koala", "1f43b": "bear", "1f437": "pig", "1f43d": "pig-nose", "1f42e": "cow", "1f417": "boar", "1f435": "monkey-face", "1f412": "monkey", "1f434": "horse", "1f40e": "racehorse", "1f42b": "camel", "1f411": "sheep", "1f418": "elephant", "1f43c": "panda-face", "1f40d": "snake", "1f426": "bird", "1f424": "baby-chick", "1f425": "hatched-chick", "1f423": "hatching-chick", "1f414": "chicken", "1f427": "penguin", "1f422": "turtle", "1f41b": "bug", "1f41d": "honeybee", "1f41c": "ant", "1f41e": "beetle", "1f40c": "snail", "1f419": "octopus", "1f420": "tropical-fish", "1f41f": "fish", "1f433": "whale", "1f40b": "whale2", "1f42c": "dolphin", "1f404": "cow2", "1f40f": "ram", "1f400": "rat", "1f403": "water-buffalo", "1f405": "tiger2", "1f407": "rabbit2", "1f409": "dragon", "1f410": "goat", "1f413": "rooster", "1f415": "dog2", "1f416": "pig2", "1f401": "mouse2", "1f402": "ox", "1f432": "dragon-face", "1f421": "blowfish", "1f40a": "crocodile", "1f42a": "dromedary-camel", "1f406": "leopard", "1f408": "cat2", "1f429": "poodle", "1f43e": "paw-prints", "1f490": "bouquet", "1f338": "cherry-blossom", "1f337": "tulip", "1f340": "four-leaf-clover", "1f339": "rose", "1f33b": "sunflower", "1f33a": "hibiscus", "1f341": "maple-leaf", "1f343": "leaves", "1f342": "fallen-leaf", "1f33f": "herb", "1f344": "mushroom", "1f335": "cactus", "1f334": "palm-tree", "1f332": "evergreen-tree", "1f333": "deciduous-tree", "1f330": "chestnut", "1f331": "seedling", "1f33c": "blossom", "1f33e": "ear-of-rice", "1f41a": "shell", "1f310": "globe-with-meridians", "1f31e": "sun-with-face", "1f31d": "full-moon-with-face", "1f31a": "new-moon-with-face", "1f311": "new-moon", "1f312": "waxing-crescent-moon", "1f313": "first-quarter-moon", "1f314": "waxing-gibbous-moon", "1f315": "full-moon", "1f316": "waning-gibbous-moon", "1f317": "last-quarter-moon", "1f318": "waning-crescent-moon", "1f31c": "last-quarter-moon-with-face", "1f31b": "first-quarter-moon-with-face", "1f319": "moon", "1f30d": "earth-africa", "1f30e": "earth-americas", "1f30f": "earth-asia", "1f30b": "volcano", "1f30c": "milky-way", "26c5": "partly-sunny", "1f38d": "bamboo", "1f49d": "gift-heart", "1f38e": "dolls", "1f392": "school-satchel", "1f393": "mortar-board", "1f38f": "flags", "1f386": "fireworks", "1f387": "sparkler", "1f390": "wind-chime", "1f391": "rice-scene", "1f383": "jack-o-lantern", "1f47b": "ghost", "1f385": "santa", "1f3b1": "8ball", "23f0": "alarm-clock", "1f34e": "apple", "1f3a8": "art", "1f37c": "baby-bottle", "1f388": "balloon", "1f34c": "banana", "1f4ca": "bar-chart", "26be": "baseball", "1f3c0": "basketball", "1f6c0": "bath", "1f6c1": "bathtub", "1f50b": "battery", "1f37a": "beer", "1f37b": "beers", "1f514": "bell", "1f371": "bento", "1f6b4": "bicyclist", "1f459": "bikini", "1f382": "birthday", "1f0cf": "black-joker", "2712": "black-nib", "1f4d8": "blue-book", "1f4a3": "bomb", "1f516": "bookmark", "1f4d1": "bookmark-tabs", "1f4da": "books", "1f462": "boot", "1f3b3": "bowling", "1f35e": "bread", "1f4bc": "briefcase", "1f4a1": "bulb", "1f370": "cake", "1f4c6": "calendar", "1f4f2": "calling", "1f4f7": "camera", "1f36c": "candy", "1f4c7": "card-index", "1f4bf": "cd", "1f4c9": "chart-with-downwards-trend", "1f4c8": "chart-with-upwards-trend", "1f352": "cherries", "1f36b": "chocolate-bar", "1f384": "christmas-tree", "1f3ac": "clapper", "1f4cb": "clipboard", "1f4d5": "closed-book", "1f510": "closed-lock-with-key", "1f302": "closed-umbrella", "2663": "clubs", "1f378": "cocktail", "2615": "coffee", "1f4bb": "computer", "1f38a": "confetti-ball", "1f36a": "cookie", "1f33d": "corn", "1f4b3": "credit-card", "1f451": "crown", "1f52e": "crystal-ball", "1f35b": "curry", "1f36e": "custard", "1f361": "dango", "1f3af": "dart", "1f4c5": "date", "2666": "diamonds", "1f4b5": "dollar", "1f6aa": "door", "1f369": "doughnut", "1f457": "dress", "1f4c0": "dvd", "1f4e7": "e-mail", "1f373": "egg", "1f346": "eggplant", "1f50c": "electric-plug", "2709": "email", "1f4b6": "euro", "1f453": "eyeglasses", "1f4e0": "fax", "1f4c1": "file-folder", "1f365": "fish-cake", "1f3a3": "fishing-pole-and-fish", "1f526": "flashlight", "1f4be": "floppy-disk", "1f3b4": "flower-playing-cards", "1f3c8": "football", "1f374": "fork-and-knife", "1f364": "fried-shrimp", "1f35f": "fries", "1f3b2": "game-die", "1f48e": "gem", "1f381": "gift", "26f3": "golf", "1f347": "grapes", "1f34f": "green-apple", "1f4d7": "green-book", "1f3b8": "guitar", "1f52b": "gun", "1f354": "hamburger", "1f528": "hammer", "1f45c": "handbag", "1f3a7": "headphones", "2665": "hearts", "1f506": "high-brightness", "1f460": "high-heel", "1f52a": "hocho", "1f36f": "honey-pot", "1f3c7": "horse-racing", "231b": "hourglass", "23f3": "hourglass-flowing-sand", "1f368": "ice-cream", "1f366": "icecream", "1f4e5": "inbox-tray", "1f4e8": "incoming-envelope", "1f4f1": "iphone", "1f456": "jeans", "1f511": "key", "1f458": "kimono", "1f4d2": "ledger", "1f34b": "lemon", "1f484": "lipstick", "1f512": "lock", "1f50f": "lock-with-ink-pen", "1f36d": "lollipop", "27bf": "loop", "1f4e2": "loudspeaker", "1f505": "low-brightness", "1f50d": "mag", "1f50e": "mag-right", "1f004": "mahjong", "1f4eb": "mailbox", "1f4ea": "mailbox-closed", "1f4ec": "mailbox-with-mail", "1f4ed": "mailbox-with-no-mail", "1f45e": "mans-shoe", "1f356": "meat-on-bone", "1f4e3": "mega", "1f348": "melon", "1f4dd": "memo", "1f3a4": "microphone", "1f52c": "microscope", "1f4bd": "minidisc", "1f4b8": "money-with-wings", "1f4b0": "moneybag", "1f6b5": "mountain-bicyclist", "1f3a5": "movie-camera", "1f3b9": "musical-keyboard", "1f3bc": "musical-score", "1f507": "mute", "1f4db": "name-badge", "1f454": "necktie", "1f4f0": "newspaper", "1f515": "no-bell", "1f4d3": "notebook", "1f4d4": "notebook-with-decorative-cover", "1f529": "nut-and-bolt", "1f362": "oden", "1f4c2": "open-file-folder", "1f4d9": "orange-book", "1f4e4": "outbox-tray", "1f4c4": "page-facing-up", "1f4c3": "page-with-curl", "1f4df": "pager", "1f4ce": "paperclip", "1f351": "peach", "1f350": "pear", "270f": "pencil2", "260e": "phone", "1f48a": "pill", "1f34d": "pineapple", "1f355": "pizza", "1f4ef": "postal-horn", "1f4ee": "postbox", "1f45d": "pouch", "1f357": "poultry-leg", "1f4b7": "pound", "1f45b": "purse", "1f4cc": "pushpin", "1f4fb": "radio", "1f35c": "ramen", "1f380": "ribbon", "1f35a": "rice", "1f359": "rice-ball", "1f358": "rice-cracker", "1f48d": "ring", "1f3c9": "rugby-football", "1f3bd": "running-shirt-with-sash", "1f376": "sake", "1f461": "sandal", "1f4e1": "satellite", "1f3b7": "saxophone", "2702": "scissors", "1f4dc": "scroll", "1f4ba": "seat", "1f367": "shaved-ice", "1f455": "shirt", "1f6bf": "shower", "1f3bf": "ski", "1f6ac": "smoking", "1f3c2": "snowboarder", "26bd": "soccer", "1f509": "sound", "1f47e": "space-invader", "2660": "spades", "1f35d": "spaghetti", "1f50a": "speaker", "1f372": "stew", "1f4cf": "straight-ruler", "1f353": "strawberry", "1f3c4": "surfer", "1f363": "sushi", "1f360": "sweet-potato", "1f3ca": "swimmer", "1f489": "syringe", "1f389": "tada", "1f38b": "tanabata-tree", "1f34a": "tangerine", "1f375": "tea", "1f4de": "telephone-receiver", "1f52d": "telescope", "1f3be": "tennis", "1f6bd": "toilet", "1f345": "tomato", "1f3a9": "tophat", "1f4d0": "triangular-ruler", "1f3c6": "trophy", "1f379": "tropical-drink", "1f3ba": "trumpet", "1f4fa": "tv", "1f513": "unlock", "1f4fc": "vhs", "1f4f9": "video-camera", "1f3ae": "video-game", "1f3bb": "violin", "231a": "watch", "1f349": "watermelon", "1f377": "wine-glass", "1f45a": "womans-clothes", "1f452": "womans-hat", "1f527": "wrench", "1f4b4": "yen", "1f6a1": "aerial-tramway", "2708": "airplane", "1f691": "ambulance", "2693": "anchor", "1f69b": "articulated-lorry", "1f3e7": "atm", "1f3e6": "bank", "1f488": "barber", "1f530": "beginner", "1f6b2": "bike", "1f699": "blue-car", "26f5": "boat", "1f309": "bridge-at-night", "1f685": "bullettrain-front", "1f684": "bullettrain-side", "1f68c": "bus", "1f68f": "busstop", "1f697": "car", "1f3a0": "carousel-horse", "1f3c1": "checkered-flag", "26ea": "church", "1f3aa": "circus-tent", "1f307": "city-sunrise", "1f306": "city-sunset", "1f6a7": "construction", "1f3ea": "convenience-store", "1f38c": "crossed-flags", "1f3ec": "department-store", "1f3f0": "european-castle", "1f3e4": "european-post-office", "1f3ed": "factory", "1f3a1": "ferris-wheel", "1f692": "fire-engine", "26f2": "fountain", "26fd": "fuelpump", "1f681": "helicopter", "1f3e5": "hospital", "1f3e8": "hotel", "2668": "hotsprings", "1f3e0": "house", "1f3e1": "house-with-garden", "1f5fe": "japan", "1f3ef": "japanese-castle", "1f688": "light-rail", "1f3e9": "love-hotel", "1f690": "minibus", "1f69d": "monorail", "1f5fb": "mount-fuji", "1f6a0": "mountain-cableway", "1f69e": "mountain-railway", "1f5ff": "moyai", "1f3e2": "office", "1f698": "oncoming-automobile", "1f68d": "oncoming-bus", "1f694": "oncoming-police-car", "1f696": "oncoming-taxi", "1f3ad": "performing-arts", "1f693": "police-car", "1f3e3": "post-office", "1f683": "railway-car", "1f308": "rainbow", "1f680": "rocket", "1f3a2": "roller-coaster", "1f6a8": "rotating-light", "1f4cd": "round-pushpin", "1f6a3": "rowboat", "1f3eb": "school", "1f6a2": "ship", "1f3b0": "slot-machine", "1f6a4": "speedboat", "1f303": "stars", "1f689": "station", "1f5fd": "statue-of-liberty", "1f682": "steam-locomotive", "1f305": "sunrise", "1f304": "sunrise-over-mountains", "1f69f": "suspension-railway", "1f695": "taxi", "26fa": "tent", "1f3ab": "ticket", "1f5fc": "tokyo-tower", "1f69c": "tractor", "1f6a5": "traffic-light", "1f686": "train2", "1f68a": "tram", "1f6a9": "triangular-flag-on-post", "1f68e": "trolleybus", "1f69a": "truck", "1f6a6": "vertical-traffic-light", "26a0": "warning", "1f492": "wedding", "1f1e6-1f1e9": "flag-ad", "1f1e6-1f1ea": "flag-ae", "1f1e6-1f1eb": "flag-af", "1f1e6-1f1ec": "flag-ag", "1f1e6-1f1ee": "flag-ai", "1f1e6-1f1f1": "flag-al", "1f1e6-1f1f2": "flag-am", "1f1e6-1f1f4": "flag-ao", "1f1e6-1f1f6": "flag-aq", "1f1e6-1f1f7": "flag-ar", "1f1e6-1f1f8": "flag-as", "1f1e6-1f1f9": "flag-at", "1f1e6-1f1fa": "flag-au", "1f1e6-1f1fc": "flag-aw", "1f1e6-1f1fd": "flag-ax", "1f1e6-1f1ff": "flag-az", "1f1e7-1f1e6": "flag-ba", "1f1e7-1f1e7": "flag-bb", "1f1e7-1f1e9": "flag-bd", "1f1e7-1f1ea": "flag-be", "1f1e7-1f1eb": "flag-bf", "1f1e7-1f1ec": "flag-bg", "1f1e7-1f1ed": "flag-bh", "1f1e7-1f1ee": "flag-bi", "1f1e7-1f1ef": "flag-bj", "1f1e7-1f1f1": "flag-bl", "1f1e7-1f1f2": "flag-bm", "1f1e7-1f1f3": "flag-bn", "1f1e7-1f1f4": "flag-bo", "1f1e7-1f1f6": "flag-bq", "1f1e7-1f1f7": "flag-br", "1f1e7-1f1f8": "flag-bs", "1f1e7-1f1f9": "flag-bt", "1f1e7-1f1fb": "flag-bv", "1f1e7-1f1fc": "flag-bw", "1f1e7-1f1fe": "flag-by", "1f1e7-1f1ff": "flag-bz", "1f1e8-1f1e6": "flag-ca", "1f1e8-1f1e8": "flag-cc", "1f1e8-1f1e9": "flag-cd", "1f1e8-1f1eb": "flag-cf", "1f1e8-1f1ec": "flag-cg", "1f1e8-1f1ed": "flag-ch", "1f1e8-1f1ee": "flag-ci", "1f1e8-1f1f0": "flag-ck", "1f1e8-1f1f1": "flag-cl", "1f1e8-1f1f2": "flag-cm", "1f1e8-1f1f3": "flag-cn", "1f1e8-1f1f4": "flag-co", "1f1e8-1f1f7": "flag-cr", "1f1e8-1f1fa": "flag-cu", "1f1e8-1f1fb": "flag-cv", "1f1e8-1f1fc": "flag-cw", "1f1e8-1f1fd": "flag-cx", "1f1e8-1f1fe": "flag-cy", "1f1e8-1f1ff": "flag-cz", "1f1e9-1f1ea": "flag-de", "1f1e9-1f1ec": "flag-dg", "1f1e9-1f1ef": "flag-dj", "1f1e9-1f1f0": "flag-dk", "1f1e9-1f1f2": "flag-dm", "1f1e9-1f1f4": "flag-do", "1f1e9-1f1ff": "flag-dz", "1f1ea-1f1e8": "flag-ec", "1f1ea-1f1ea": "flag-ee", "1f1ea-1f1ec": "flag-eg", "1f1ea-1f1ed": "flag-eh", "1f1ea-1f1f7": "flag-er", "1f1ea-1f1f8": "flag-es", "1f1ea-1f1fa": "flag-eu", "1f1ea-1f1f9": "flag-et", "1f1eb-1f1ee": "flag-fi", "1f1eb-1f1ef": "flag-fj", "1f1eb-1f1f0": "flag-fk", "1f1eb-1f1f2": "flag-fm", "1f1eb-1f1f4": "flag-fo", "1f1eb-1f1f7": "flag-fr", "1f1ec-1f1e6": "flag-ga", "1f1ec-1f1e7": "flag-gb", "1f1ec-1f1e9": "flag-gd", "1f1ec-1f1ea": "flag-ge", "1f1ec-1f1eb": "flag-gf", "1f1ec-1f1ec": "flag-gg", "1f1ec-1f1ed": "flag-gh", "1f1ec-1f1ee": "flag-gi", "1f1ec-1f1f1": "flag-gl", "1f1ec-1f1f2": "flag-gm", "1f1ec-1f1f3": "flag-gn", "1f1ec-1f1f5": "flag-gp", "1f1ec-1f1f6": "flag-gq", "1f1ec-1f1f7": "flag-gr", "1f1ec-1f1f8": "flag-gs", "1f1ec-1f1f9": "flag-gt", "1f1ec-1f1fa": "flag-gu", "1f1ec-1f1fc": "flag-gw", "1f1ec-1f1fe": "flag-gy", "1f1ed-1f1f0": "flag-hk", "1f1ed-1f1f2": "flag-hm", "1f1ed-1f1f3": "flag-hn", "1f1ed-1f1f7": "flag-hr", "1f1ed-1f1f9": "flag-ht", "1f1ed-1f1fa": "flag-hu", "1f1ee-1f1e9": "flag-id", "1f1ee-1f1ea": "flag-ie", "1f1ee-1f1f1": "flag-il", "1f1ee-1f1f2": "flag-im", "1f1ee-1f1f3": "flag-in", "1f1ee-1f1f4": "flag-io", "1f1ee-1f1f6": "flag-iq", "1f1ee-1f1f7": "flag-ir", "1f1ee-1f1f8": "flag-is", "1f1ee-1f1f9": "flag-it", "1f1ef-1f1ea": "flag-je", "1f1ef-1f1f2": "flag-jm", "1f1ef-1f1f4": "flag-jo", "1f1ef-1f1f5": "flag-jp", "1f1f0-1f1ea": "flag-ke", "1f1f0-1f1ec": "flag-kg", "1f1f0-1f1ed": "flag-kh", "1f1f0-1f1ee": "flag-ki", "1f1f0-1f1f2": "flag-km", "1f1f0-1f1f3": "flag-kn", "1f1f0-1f1f5": "flag-kp", "1f1f0-1f1f7": "flag-kr", "1f1f0-1f1fc": "flag-kw", "1f1f0-1f1fe": "flag-ky", "1f1f0-1f1ff": "flag-kz", "1f1f1-1f1e6": "flag-la", "1f1f1-1f1e7": "flag-lb", "1f1f1-1f1e8": "flag-lc", "1f1f1-1f1ee": "flag-li", "1f1f1-1f1f0": "flag-lk", "1f1f1-1f1f7": "flag-lr", "1f1f1-1f1f8": "flag-ls", "1f1f1-1f1f9": "flag-lt", "1f1f1-1f1fa": "flag-lu", "1f1f1-1f1fb": "flag-lv", "1f1f1-1f1fe": "flag-ly", "1f1f2-1f1e6": "flag-ma", "1f1f2-1f1e8": "flag-mc", "1f1f2-1f1e9": "flag-md", "1f1f2-1f1ea": "flag-me", "1f1f2-1f1eb": "flag-mf", "1f1f2-1f1ec": "flag-mg", "1f1f2-1f1ed": "flag-mh", "1f1f2-1f1f0": "flag-mk", "1f1f2-1f1f1": "flag-ml", "1f1f2-1f1f2": "flag-mm", "1f1f2-1f1f3": "flag-mn", "1f1f2-1f1f4": "flag-mo", "1f1f2-1f1f5": "flag-mp", "1f1f2-1f1f6": "flag-mq", "1f1f2-1f1f7": "flag-mr", "1f1f2-1f1f8": "flag-ms", "1f1f2-1f1f9": "flag-mt", "1f1f2-1f1fa": "flag-mu", "1f1f2-1f1fb": "flag-mv", "1f1f2-1f1fc": "flag-mw", "1f1f2-1f1fd": "flag-mx", "1f1f2-1f1fe": "flag-my", "1f1f2-1f1ff": "flag-mz", "1f1f3-1f1e6": "flag-na", "1f1f3-1f1e8": "flag-nc", "1f1f3-1f1ea": "flag-ne", "1f1f3-1f1eb": "flag-nf", "1f1f3-1f1ec": "flag-ng", "1f1f3-1f1ee": "flag-ni", "1f1f3-1f1f1": "flag-nl", "1f1f3-1f1f4": "flag-no", "1f1f3-1f1f5": "flag-np", "1f1f3-1f1f7": "flag-nr", "1f1f3-1f1fa": "flag-nu", "1f1f3-1f1ff": "flag-nz", "1f1f4-1f1f2": "flag-om", "1f1f5-1f1e6": "flag-pa", "1f1f5-1f1ea": "flag-pe", "1f1f5-1f1eb": "flag-pf", "1f1f5-1f1ec": "flag-pg", "1f1f5-1f1ed": "flag-ph", "1f1f5-1f1f0": "flag-pk", "1f1f5-1f1f1": "flag-pl", "1f1f5-1f1f2": "flag-pm", "1f1f5-1f1f3": "flag-pn", "1f1f5-1f1f7": "flag-pr", "1f1f5-1f1f8": "flag-ps", "1f1f5-1f1f9": "flag-pt", "1f1f5-1f1fc": "flag-pw", "1f1f5-1f1fe": "flag-py", "1f1f6-1f1e6": "flag-qa", "1f1f7-1f1ea": "flag-re", "1f1f7-1f1f4": "flag-ro", "1f1f7-1f1f8": "flag-rs", "1f1f7-1f1fa": "flag-ru", "1f1f7-1f1fc": "flag-rw", "1f1f8-1f1e6": "flag-sa", "1f1f8-1f1e7": "flag-sb", "1f1f8-1f1e8": "flag-sc", "1f1f8-1f1e9": "flag-sd", "1f1f8-1f1ea": "flag-se", "1f1f8-1f1ec": "flag-sg", "1f1f8-1f1ed": "flag-sh", "1f1f8-1f1ee": "flag-si", "1f1f8-1f1ef": "flag-sj", "1f1f8-1f1f0": "flag-sk", "1f1f8-1f1f1": "flag-sl", "1f1f8-1f1f2": "flag-sm", "1f1f8-1f1f3": "flag-sn", "1f1f8-1f1f4": "flag-so", "1f1f8-1f1f7": "flag-sr", "1f1f8-1f1f8": "flag-ss", "1f1f8-1f1f9": "flag-st", "1f1f8-1f1fb": "flag-sv", "1f1f8-1f1fd": "flag-sx", "1f1f8-1f1fe": "flag-sy", "1f1f8-1f1ff": "flag-sz", "1f1f9-1f1e8": "flag-tc", "1f1f9-1f1e9": "flag-td", "1f1f9-1f1eb": "flag-tf", "1f1f9-1f1ec": "flag-tg", "1f1f9-1f1ed": "flag-th", "1f1f9-1f1ef": "flag-tj", "1f1f9-1f1f0": "flag-tk", "1f1f9-1f1f1": "flag-tl", "1f1f9-1f1f2": "flag-tm", "1f1f9-1f1f3": "flag-tn", "1f1f9-1f1f4": "flag-to", "1f1f9-1f1f7": "flag-tr", "1f1f9-1f1f9": "flag-tt", "1f1f9-1f1fb": "flag-tv", "1f1f9-1f1fc": "flag-tw", "1f1f9-1f1ff": "flag-tz", "1f1fa-1f1e6": "flag-ua", "1f1fa-1f1ec": "flag-ug", "1f1fa-1f1f2": "flag-um", "1f1fa-1f1f8": "flag-us", "1f1fa-1f1fe": "flag-uy", "1f1fa-1f1ff": "flag-uz", "1f1fb-1f1e6": "flag-va", "1f1fb-1f1e8": "flag-vc", "1f1fb-1f1ea": "flag-ve", "1f1fb-1f1ec": "flag-vg", "1f1fb-1f1ee": "flag-vi", "1f1fb-1f1f3": "flag-vn", "1f1fb-1f1fa": "flag-vu", "1f1fc-1f1eb": "flag-wf", "1f1fc-1f1f8": "flag-ws", "1f1fd-1f1f0": "flag-xk", "1f1fe-1f1ea": "flag-ye", "1f1fe-1f1f9": "flag-yt", "1f1ff-1f1e6": "flag-za", "1f1ff-1f1f2": "flag-zm", "1f1ff-1f1fc": "flag-zw", "1f4af": "100", "1f522": "1234", "1f170": "a", "1f18e": "ab", "1f524": "abc", "1f521": "abcd", "1f251": "accept", "2652": "aquarius", "2648": "aries", "25c0": "arrow-backward", "23ec": "arrow-double-down", "23eb": "arrow-double-up", "2b07": "arrow-down", "1f53d": "arrow-down-small", "25b6": "arrow-forward", "2935": "arrow-heading-down", "2934": "arrow-heading-up", "2b05": "arrow-left", "2199": "arrow-lower-left", "2198": "arrow-lower-right", "27a1": "arrow-right", "21aa": "arrow-right-hook", "2b06": "arrow-up", "2195": "arrow-up-down", "1f53c": "arrow-up-small", "2196": "arrow-upper-left", "2197": "arrow-upper-right", "1f503": "arrows-clockwise", "1f504": "arrows-counterclockwise", "1f171": "b", "1f6bc": "baby-symbol", "1f6c4": "baggage-claim", "2611": "ballot-box-with-check", "203c": "bangbang", "26ab": "black-circle", "1f532": "black-square-button", "264b": "cancer", "1f520": "capital-abcd", "2651": "capricorn", "1f4b9": "chart", "1f6b8": "children-crossing", "1f3a6": "cinema", "1f191": "cl", "1f550": "clock1", "1f559": "clock10", "1f565": "clock1030", "1f55a": "clock11", "1f566": "clock1130", "1f55b": "clock12", "1f567": "clock1230", "1f55c": "clock130", "1f551": "clock2", "1f55d": "clock230", "1f552": "clock3", "1f55e": "clock330", "1f553": "clock4", "1f55f": "clock430", "1f554": "clock5", "1f560": "clock530", "1f555": "clock6", "1f561": "clock630", "1f556": "clock7", "1f562": "clock730", "1f557": "clock8", "1f563": "clock830", "1f558": "clock9", "1f564": "clock930", "3297": "congratulations", "1f192": "cool", "a9": "copyright", "27b0": "curly-loop", "1f4b1": "currency-exchange", "1f6c3": "customs", "1f4a0": "diamond-shape-with-a-dot-inside", "1f6af": "do-not-litter", "38-20e3": "eight", "2734": "eight-pointed-black-star", "2733": "eight-spoked-asterisk", "1f51a": "end", "23e9": "fast-forward", "35-20e3": "five", "34-20e3": "four", "1f193": "free", "264a": "gemini", "23-20e3": "hash", "1f49f": "heart-decoration", "2714": "heavy-check-mark", "2797": "heavy-division-sign", "1f4b2": "heavy-dollar-sign", "2796": "heavy-minus-sign", "2716": "heavy-multiplication-x", "2795": "heavy-plus-sign", "1f194": "id", "1f250": "ideograph-advantage", "2139": "information-source", "2049": "interrobang", "1f51f": "keycap-ten", "1f201": "koko", "1f535": "large-blue-circle", "1f537": "large-blue-diamond", "1f536": "large-orange-diamond", "1f6c5": "left-luggage", "2194": "left-right-arrow", "21a9": "leftwards-arrow-with-hook", "264c": "leo", "264e": "libra", "1f517": "link", "24c2": "m", "1f6b9": "mens", "1f687": "metro", "1f4f4": "mobile-phone-off", "274e": "negative-squared-cross-mark", "1f195": "new", "1f196": "ng", "39-20e3": "nine", "1f6b3": "no-bicycles", "26d4": "no-entry", "1f6ab": "no-entry-sign", "1f4f5": "no-mobile-phones", "1f6b7": "no-pedestrians", "1f6ad": "no-smoking", "1f6b1": "non-potable-water", "2b55": "o", "1f17e": "o2", "1f197": "ok", "1f51b": "on", "31-20e3": "one", "26ce": "ophiuchus", "1f17f": "parking", "303d": "part-alternation-mark", "1f6c2": "passport-control", "2653": "pisces", "1f6b0": "potable-water", "1f6ae": "put-litter-in-its-place", "1f518": "radio-button", "267b": "recycle", "1f534": "red-circle", "ae": "registered", "1f501": "repeat", "1f502": "repeat-one", "1f6bb": "restroom", "23ea": "rewind", "1f202": "sa", "2650": "sagittarius", "264f": "scorpius", "3299": "secret", "37-20e3": "seven", "1f4f6": "signal-strength", "36-20e3": "six", "1f52f": "six-pointed-star", "1f539": "small-blue-diamond", "1f538": "small-orange-diamond", "1f53a": "small-red-triangle", "1f53b": "small-red-triangle-down", "1f51c": "soon", "1f198": "sos", "1f523": "symbols", "2649": "taurus", "33-20e3": "three", "2122": "tm", "1f51d": "top", "1f531": "trident", "1f500": "twisted-rightwards-arrows", "32-20e3": "two", "1f239": "u5272", "1f234": "u5408", "1f23a": "u55b6", "1f22f": "u6307", "1f237": "u6708", "1f236": "u6709", "1f235": "u6e80", "1f21a": "u7121", "1f238": "u7533", "1f232": "u7981", "1f233": "u7a7a", "1f51e": "underage", "1f199": "up", "1f4f3": "vibration-mode", "264d": "virgo", "1f19a": "vs", "3030": "wavy-dash", "1f6be": "wc", "267f": "wheelchair", "2705": "white-check-mark", "26aa": "white-circle", "1f4ae": "white-flower", "1f533": "white-square-button", "1f6ba": "womens", "274c": "x", "30-20e3": "zero" ); @each $code, $name in $emoji-map { .twa-#{$name} { background-image: url("https://twemoji.maxcdn.com/svg/#{$code}.svg"); } } ================================================ FILE: client/modules/boot.js ================================================ export default { readyStates: [], callbacks: [], /** * Check if event has been sent * * @param {String} evt Event name * @returns {Boolean} True if fired */ isReady (evt) { return this.readyStates.indexOf(evt) >= 0 }, /** * Register a callback to be executed when event is sent * * @param {String} evt Event name to register to * @param {Function} clb Callback function * @param {Boolean} once If the callback should be called only once */ register (evt, clb, once) { if (this.isReady(evt)) { clb() } else { this.callbacks.push({ event: evt, callback: clb, once: false, called: false }) } }, /** * Register a callback to be executed only once when event is sent * * @param {String} evt Event name to register to * @param {Function} clb Callback function */ registerOnce (evt, clb) { this.register(evt, clb, true) }, /** * Set ready state and execute callbacks */ notify (evt) { this.readyStates.push(evt) this.callbacks.forEach(clb => { if (clb.event === evt) { if (clb.once && clb.called) { return } clb.called = true clb.callback() } }) }, /** * Execute callback on DOM ready * * @param {Function} clb Callback function */ onDOMReady (clb) { if (document.readyState === 'interactive' || document.readyState === 'complete' || document.readyState === 'loaded') { clb() } else { document.addEventListener('DOMContentLoaded', clb) } } } ================================================ FILE: client/modules/localization.js ================================================ import i18next from 'i18next' import Backend from 'i18next-chained-backend' import LocalStorageBackend from 'i18next-localstorage-backend' import i18nextXHR from 'i18next-xhr-backend' import VueI18Next from '@panter/vue-i18next' import _ from 'lodash' /* global siteConfig, graphQL */ import localeQuery from 'gql/common/common-localization-query-translations.gql' export default { VueI18Next, init() { i18next .use(Backend) .init({ backend: { backends: [ LocalStorageBackend, i18nextXHR ], backendOptions: [ { expirationTime: 1000 * 60 * 60 * 24 // 24h }, { loadPath: '{{lng}}/{{ns}}', parse: (data) => data, ajax: (url, opts, cb, data) => { let langParams = url.split('/') graphQL.query({ query: localeQuery, variables: { locale: langParams[0], namespace: langParams[1] } }).then(resp => { let ns = {} if (_.get(resp, 'data.localization.translations', []).length > 0) { resp.data.localization.translations.forEach(entry => { _.set(ns, entry.key, entry.value) }) } return cb(ns, {status: '200'}) }).catch(err => { console.error(err) return cb(null, {status: '404'}) }) } } ] }, defaultNS: 'common', lng: siteConfig.lang, load: 'currentOnly', lowerCaseLng: true, fallbackLng: siteConfig.lang, ns: ['common', 'auth'] }) return new VueI18Next(i18next) } } ================================================ FILE: client/polyfills/array-from.js ================================================ // Production steps of ECMA-262, Edition 6, 22.1.2.1 if (!Array.from) { Array.from = (function () { var toStr = Object.prototype.toString var isCallable = function (fn) { return typeof fn === 'function' || toStr.call(fn) === '[object Function]' } var toInteger = function (value) { var number = Number(value) if (isNaN(number)) { return 0 } if (number === 0 || !isFinite(number)) { return number } return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)) } var maxSafeInteger = Math.pow(2, 53) - 1 var toLength = function (value) { var len = toInteger(value) return Math.min(Math.max(len, 0), maxSafeInteger) } // The length property of the from method is 1. return function from (arrayLike/*, mapFn, thisArg */) { // 1. Let C be the this value. var C = this // 2. Let items be ToObject(arrayLike). var items = Object(arrayLike) // 3. ReturnIfAbrupt(items). if (arrayLike == null) { throw new TypeError('Array.from requires an array-like object - not null or undefined') } // 4. If mapfn is undefined, then let mapping be false. var mapFn = arguments.length > 1 ? arguments[1] : void undefined var T if (typeof mapFn !== 'undefined') { // 5. else // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. if (!isCallable(mapFn)) { throw new TypeError('Array.from: when provided, the second argument must be a function') } // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. if (arguments.length > 2) { T = arguments[2] } } // 10. Let lenValue be Get(items, "length"). // 11. Let len be ToLength(lenValue). var len = toLength(items.length) // 13. If IsConstructor(C) is true, then // 13. a. Let A be the result of calling the [[Construct]] internal method // of C with an argument list containing the single item len. // 14. a. Else, Let A be ArrayCreate(len). var A = isCallable(C) ? Object(new C(len)) : new Array(len) // 16. Let k be 0. var k = 0 // 17. Repeat, while k < len… (also steps a - h) var kValue while (k < len) { kValue = items[k] if (mapFn) { A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k) } else { A[k] = kValue } k += 1 } // 18. Let putStatus be Put(A, "length", len, true). A.length = len // 20. Return A. return A } }()) } ================================================ FILE: client/scss/app.scss ================================================ @import "global"; @import "base/base"; @import "base/icons"; @import "base/animation"; @import '~vuescroll/dist/vuescroll.css'; @import '~katex/dist/katex.min.css'; @import '~diff2html/bundles/css/diff2html.min.css'; @import 'components/codemirror'; @import 'components/katex'; @import 'components/v-btn'; @import 'components/v-data-table'; @import 'components/v-dialog'; @import 'components/v-form'; @import 'components/v-tabs'; // @import '../libs/twemoji/twemoji-awesome'; // @import '../libs/prism/prism.css'; // @import '~vue-tour/dist/vue-tour.css'; // @import '~xterm/dist/xterm.css'; // @import 'node_modules/diff2html/dist/diff2html.min'; @import 'pages/new'; @import 'pages/notfound'; @import 'pages/unauthorized'; @import 'pages/welcome'; @import 'pages/error'; @import 'layout/_rtl'; ================================================ FILE: client/scss/base/animation.scss ================================================ $use-fade: true; $use-zoom: true; $use-bounce: true; @import "~animate-sass/animate"; @for $i from 1 to 12 { .wait-p#{$i}s { animation-delay: $i * .1s !important; } } ================================================ FILE: client/scss/base/base.scss ================================================ html { box-sizing: border-box; height: 100%; overflow-y: auto !important; } *, *:before, *:after { box-sizing: inherit; } [v-cloak], .is-hidden { display: none; } #root { position: relative; min-height: 100%; &.is-fullscreen { height: 100vh; } } .v-application--wrap { transition: all 1.2s ease; transform-origin: 50% 50%; // background-color: #FFF; @at-root .theme--dark & { background-color: mc('grey', '900'); } } @media only screen and (min-width:960px) { .v-application .v-footer { padding-left: 272px } } #root .v-application { .overline { line-height: 1rem; font-size: .625rem!important; font-weight: 400; letter-spacing: .1666666667em!important; } @for $i from 0 through 25 { .radius-#{$i} { border-radius: #{$i}px; } } @for $i from 1 through 5 { .grey.darken-2-d#{$i} { background-color: darken(mc('grey', '700'), percentage($i/100)) !important; border-color: darken(mc('grey', '700'), percentage($i/100)) !important; } .grey.darken-2-l#{$i} { background-color: lighten(mc('grey', '700'), percentage($i/100)) !important; border-color: lighten(mc('grey', '700'), percentage($i/100)) !important; } .grey.darken-3-d#{$i} { background-color: darken(mc('grey', '800'), percentage($i/100)) !important; border-color: darken(mc('grey', '800'), percentage($i/100)) !important; } .grey.darken-3-l#{$i} { background-color: lighten(mc('grey', '800'), percentage($i/100)) !important; border-color: lighten(mc('grey', '800'), percentage($i/100)) !important; } .grey.darken-4-d#{$i} { background-color: darken(mc('grey', '900'), percentage($i/100)) !important; border-color: darken(mc('grey', '900'), percentage($i/100)) !important; } .grey.darken-4-l#{$i} { background-color: lighten(mc('grey', '900'), percentage($i/100)) !important; border-color: lighten(mc('grey', '900'), percentage($i/100)) !important; } } .grey.darken-5 { background-color: #0C0C0C !important; border-color: #0C0C0C !important; } .blue.darken-5 { background-color: darken(mc('blue', '900'), 20%) !important; border-color: darken(mc('blue', '900'), 20%) !important; } .indigo.darken-5 { background-color: darken(mc('indigo', '900'), 10%) !important; border-color: darken(mc('indigo', '900'), 10%) !important; } } ================================================ FILE: client/scss/base/icons.scss ================================================ // @font-face { // font-family: 'Material Icons'; // font-style: normal; // font-weight: 400; // src: local('Material Icons'), // local('MaterialIcons-Regular'), // url(/fonts/MaterialIcons-Regular.woff2) format('woff2'), // url(/fonts/MaterialIcons-Regular.woff) format('woff'); // } // .material-icons { // font-family: 'Material Icons', sans-serif; // font-weight: normal; // font-style: normal; // font-size: 24px; /* Preferred icon size */ // display: inline-flex; // line-height: 1; // text-transform: none; // letter-spacing: normal; // word-wrap: normal; // white-space: nowrap; // direction: ltr; // /* Support for all WebKit browsers. */ // -webkit-font-smoothing: antialiased; // /* Support for Safari and Chrome. */ // text-rendering: optimizeLegibility; // /* Support for Firefox. */ // -moz-osx-font-smoothing: grayscale; // /* Support for IE. */ // font-feature-settings: 'liga'; // } .icons { display: inline-block; color: mc('grey', '800'); &.is-text { display: inline-block; width: 1em; height: 1em; vertical-align: middle; position: relative; top: -0.0625em; stroke: none; fill: none; } @each $size in 16,18,20,24,32,48,64,96,128 { &.is-#{$size} { width: #{$size}px; height: #{$size}px; } } &.has-right-pad { margin-right: .5rem; } &.is-outlined { stroke-width: 2px; use { fill: inherit; stroke: mc('grey', '800'); } } } .material-design-icon { display: inline-flex; } ================================================ FILE: client/scss/base/material.scss ================================================ $material-colors: ( 'red': ( '50': #ffebee, '100': #ffcdd2, '200': #ef9a9a, '300': #e57373, '400': #ef5350, '500': #f44336, '600': #e53935, '700': #d32f2f, '800': #c62828, '900': #b71c1c, 'a100': #ff8a80, 'a200': #ff5252, 'a400': #ff1744, 'a700': #d50000 ), 'pink': ( '50': #fce4ec, '100': #f8bbd0, '200': #f48fb1, '300': #f06292, '400': #ec407a, '500': #e91e63, '600': #d81b60, '700': #c2185b, '800': #ad1457, '900': #880e4f, 'a100': #ff80ab, 'a200': #ff4081, 'a400': #f50057, 'a700': #c51162 ), 'purple': ( '50': #f3e5f5, '100': #e1bee7, '200': #ce93d8, '300': #ba68c8, '400': #ab47bc, '500': #9c27b0, '600': #8e24aa, '700': #7b1fa2, '800': #6a1b9a, '900': #4a148c, 'a100': #ea80fc, 'a200': #e040fb, 'a400': #d500f9, 'a700': #aa00ff ), 'deep-purple': ( '50': #ede7f6, '100': #d1c4e9, '200': #b39ddb, '300': #9575cd, '400': #7e57c2, '500': #673ab7, '600': #5e35b1, '700': #512da8, '800': #4527a0, '900': #311b92, 'a100': #b388ff, 'a200': #7c4dff, 'a400': #651fff, 'a700': #6200ea ), 'indigo': ( '50': #e8eaf6, '100': #c5cae9, '200': #9fa8da, '300': #7986cb, '400': #5c6bc0, '500': #3f51b5, '600': #3949ab, '700': #303f9f, '800': #283593, '900': #1a237e, 'a100': #8c9eff, 'a200': #536dfe, 'a400': #3d5afe, 'a700': #304ffe ), 'blue': ( '50': #e3f2fd, '100': #bbdefb, '200': #90caf9, '300': #64b5f6, '400': #42a5f5, '500': #2196f3, '600': #1e88e5, '700': #1976d2, '800': #1565c0, '900': #0d47a1, 'a100': #82b1ff, 'a200': #448aff, 'a400': #2979ff, 'a700': #2962ff ), 'light-blue': ( '50': #e1f5fe, '100': #b3e5fc, '200': #81d4fa, '300': #4fc3f7, '400': #29b6f6, '500': #03a9f4, '600': #039be5, '700': #0288d1, '800': #0277bd, '900': #01579b, 'a100': #80d8ff, 'a200': #40c4ff, 'a400': #00b0ff, 'a700': #0091ea ), 'cyan': ( '50': #e0f7fa, '100': #b2ebf2, '200': #80deea, '300': #4dd0e1, '400': #26c6da, '500': #00bcd4, '600': #00acc1, '700': #0097a7, '800': #00838f, '900': #006064, 'a100': #84ffff, 'a200': #18ffff, 'a400': #00e5ff, 'a700': #00b8d4 ), 'teal': ( '50': #e0f2f1, '100': #b2dfdb, '200': #80cbc4, '300': #4db6ac, '400': #26a69a, '500': #009688, '600': #00897b, '700': #00796b, '800': #00695c, '900': #004d40, 'a100': #a7ffeb, 'a200': #64ffda, 'a400': #1de9b6, 'a700': #00bfa5 ), 'green': ( '50': #e8f5e9, '100': #c8e6c9, '200': #a5d6a7, '300': #81c784, '400': #66bb6a, '500': #4caf50, '600': #43a047, '700': #388e3c, '800': #2e7d32, '900': #1b5e20, 'a100': #b9f6ca, 'a200': #69f0ae, 'a400': #00e676, 'a700': #00c853 ), 'light-green': ( '50': #f1f8e9, '100': #dcedc8, '200': #c5e1a5, '300': #aed581, '400': #9ccc65, '500': #8bc34a, '600': #7cb342, '700': #689f38, '800': #558b2f, '900': #33691e, 'a100': #ccff90, 'a200': #b2ff59, 'a400': #76ff03, 'a700': #64dd17 ), 'lime': ( '50': #f9fbe7, '100': #f0f4c3, '200': #e6ee9c, '300': #dce775, '400': #d4e157, '500': #cddc39, '600': #c0ca33, '700': #afb42b, '800': #9e9d24, '900': #827717, 'a100': #f4ff81, 'a200': #eeff41, 'a400': #c6ff00, 'a700': #aeea00 ), 'yellow': ( '50': #fffde7, '100': #fff9c4, '200': #fff59d, '300': #fff176, '400': #ffee58, '500': #ffeb3b, '600': #fdd835, '700': #fbc02d, '800': #f9a825, '900': #f57f17, 'a100': #ffff8d, 'a200': #ffff00, 'a400': #ffea00, 'a700': #ffd600 ), 'amber': ( '50': #fff8e1, '100': #ffecb3, '200': #ffe082, '300': #ffd54f, '400': #ffca28, '500': #ffc107, '600': #ffb300, '700': #ffa000, '800': #ff8f00, '900': #ff6f00, 'a100': #ffe57f, 'a200': #ffd740, 'a400': #ffc400, 'a700': #ffab00 ), 'orange': ( '50': #fff3e0, '100': #ffe0b2, '200': #ffcc80, '300': #ffb74d, '400': #ffa726, '500': #ff9800, '600': #fb8c00, '700': #f57c00, '800': #ef6c00, '900': #e65100, 'a100': #ffd180, 'a200': #ffab40, 'a400': #ff9100, 'a700': #ff6d00 ), 'deep-orange': ( '50': #fbe9e7, '100': #ffccbc, '200': #ffab91, '300': #ff8a65, '400': #ff7043, '500': #ff5722, '600': #f4511e, '700': #e64a19, '800': #d84315, '900': #bf360c, 'a100': #ff9e80, 'a200': #ff6e40, 'a400': #ff3d00, 'a700': #dd2c00 ), 'brown': ( '50': #efebe9, '100': #d7ccc8, '200': #bcaaa4, '300': #a1887f, '400': #8d6e63, '500': #795548, '600': #6d4c41, '700': #5d4037, '800': #4e342e, '900': #3e2723 ), 'grey': ( '50': #fafafa, '100': #f5f5f5, '200': #eeeeee, '300': #e0e0e0, '400': #bdbdbd, '500': #9e9e9e, '600': #757575, '700': #616161, '800': #424242, '900': #212121 ), 'blue-grey': ( '50': #eceff1, '100': #cfd8dc, '200': #b0bec5, '300': #90a4ae, '400': #78909c, '500': #607d8b, '600': #546e7a, '700': #455a64, '800': #37474f, '900': #263238, '1000': #11171a ), 'theme': ( 'primary': #1976D2, 'secondary': #424242, 'accent': #82B1FF, 'error': #FF5252, 'info': #2196F3, 'success': #4CAF50, 'warning': #FFC107 ) ); @function mc($color-name, $color-variant: '500') { $color: map-get(map-get($material-colors, $color-name),$color-variant); @if $color { @return $color; } @else { // Libsass still doesn't seem to support @error @warn "=> ERROR: COLOR NOT FOUND! <= | Your $color-name, $color-variant combination did not match any of the values in the $material-colors map."; } } ================================================ FILE: client/scss/base/mixins.scss ================================================ /** * Placeholder attribute for inputs * * @return {string} Placeholder attributes */ @mixin placeholder { &::-webkit-input-placeholder {@content;} &::-moz-placeholder {@content;} &:-ms-input-placeholder {@content;} &:placeholder-shown {@content;} } /** * Spinner element * * @param {string} $color - Color * @param {string} $dur - Animation Duration * @param {int} $width - Width * @param {int} $height [$width] - height * * @return {string} Spinner element */ @mixin spinner($color,$dur,$width,$height:$width) { width: $width; height: $height; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0,0,0,0.1), 2px 1px 0 $color; @include prefix(animation, spin $dur linear infinite); @include keyframes(spin) { 100%{ @include prefix(transform, rotate(360deg)); } } } /** * Prefixes for keyframes * * @param {string} $animation-name - The animation name * * @return {string} Prefixed keyframes attributes */ @mixin keyframes($animation-name) { @-webkit-keyframes #{$animation-name} { @content; } @-moz-keyframes #{$animation-name} { @content; } @-o-keyframes #{$animation-name} { @content; } @keyframes #{$animation-name} { @content; } } /** * Prefix function for browser compatibility * * @param {string} $property - Property name * @param {any} $value - Property value * * @return {string} Prefixed attributes */ @mixin prefix($property, $value) { -webkit-#{$property}: #{$value}; -moz-#{$property}: #{$value}; -ms-#{$property}: #{$value}; -o-#{$property}: #{$value}; #{$property}: #{$value}; } /** * Layout Mixins */ @mixin from($device) { @media screen and (min-width: $device) { @content; } } @mixin until($device) { @media screen and (max-width: $device - 1px) { @content; } } @mixin mobile { @media screen and (max-width: $tablet - 1px) { @content; } } @mixin tablet { @media screen and (min-width: $tablet) { @content; } } @mixin tablet-only { @media screen and (min-width: $tablet) and (max-width: $desktop - 1px) { @content; } } @mixin touch { @media screen and (max-width: $desktop - 1px) { @content; } } @mixin desktop { @media screen and (min-width: $desktop) { @content; } } @mixin desktop-only { @media screen and (min-width: $desktop) and (max-width: $widescreen - 1px) { @content; } } @mixin widescreen { @media screen and (min-width: $widescreen) { @content; } } // Nucleo Icons @mixin nc-rotate($degrees, $rotation) { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); -webkit-transform: rotate($degrees); -moz-transform: rotate($degrees); -ms-transform: rotate($degrees); -o-transform: rotate($degrees); transform: rotate($degrees); } @mixin nc-flip($horiz, $vert, $rotation) { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); -webkit-transform: scale($horiz, $vert); -moz-transform: scale($horiz, $vert); -ms-transform: scale($horiz, $vert); -o-transform: scale($horiz, $vert); transform: scale($horiz, $vert); } ================================================ FILE: client/scss/components/codemirror.scss ================================================ .cm-s-wikijs-dark.CodeMirror { background: darken(mc('grey','900'), 3%); color: #e0e0e0; } .cm-s-wikijs-dark div.CodeMirror-selected { background: mc('blue','800'); } .cm-s-wikijs-dark .cm-matchhighlight { background: mc('blue','800'); } .cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection { background: mc('amber', '500'); } .cm-s-wikijs-dark .CodeMirror-line::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::-moz-selection { background: mc('amber', '500'); } .cm-s-wikijs-dark .CodeMirror-gutters { background: darken(mc('grey','900'), 6%); border-right: 1px solid mc('grey','900'); } .cm-s-wikijs-dark .CodeMirror-guttermarker { color: #ac4142; } .cm-s-wikijs-dark .CodeMirror-guttermarker-subtle { color: #505050; } .cm-s-wikijs-dark .CodeMirror-linenumber { color: mc('grey','800'); } .cm-s-wikijs-dark .CodeMirror-cursor { border-left: 1px solid #b0b0b0; } .cm-s-wikijs-dark span.cm-comment { color: mc('orange','800'); } .cm-s-wikijs-dark span.cm-atom { color: #aa759f; } .cm-s-wikijs-dark span.cm-number { color: #aa759f; } .cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute { color: #90a959; } .cm-s-wikijs-dark span.cm-keyword { color: #ac4142; } .cm-s-wikijs-dark span.cm-string { color: #f4bf75; } .cm-s-wikijs-dark span.cm-variable { color: #90a959; } .cm-s-wikijs-dark span.cm-variable-2 { color: #6a9fb5; } .cm-s-wikijs-dark span.cm-def { color: #d28445; } .cm-s-wikijs-dark span.cm-bracket { color: #e0e0e0; } .cm-s-wikijs-dark span.cm-tag { color: #ac4142; } .cm-s-wikijs-dark span.cm-link { color: #aa759f; } .cm-s-wikijs-dark span.cm-error { background: #ac4142; color: #b0b0b0; } .cm-s-wikijs-dark .CodeMirror-activeline-background { background: mc('grey','900'); } .cm-s-wikijs-dark .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } .cm-s-wikijs-dark .CodeMirror-foldmarker { margin-left: 10px; display: inline-block; background-color: rgba(mc('amber', '800'), .3); padding: 8px 5px; color: mc('amber', '500'); border-radius: 5px; text-shadow: none; } .cm-s-wikijs-dark .CodeMirror-buttonmarker { display: inline-block; background-color: rgba(mc('blue', '500'), .3); border: 1px solid mc('blue', '800'); padding: 1px 10px; color: mc('blue', '200') !important; border-radius: 5px; margin-left: 5px; cursor: pointer; } ================================================ FILE: client/scss/components/katex.scss ================================================ .v-application .katex .accent { background-color: inherit !important; border-color: inherit !important; } ================================================ FILE: client/scss/components/v-btn.scss ================================================ .v-btn.is-icon { min-width: auto; } .btn-animate-rotate { i { transition: all 4s ease; transform: rotate(0deg); } &:hover i { transform: rotate(360deg); } } .btn-animate-grow { i { transition: all 2s ease; transform: scale(1); } &:hover i { transform: scale(1.25); } } .btn-animate-edit { i { transition: all .7s cubic-bezier(0.68, -0.55, 0.265, 1.55); transform: rotate(0deg); } &:hover i { transform: rotate(-45deg); } } .btn-animate-wrench { i { transition: all .7s cubic-bezier(0.68, -0.55, 0.265, 1.55); transform: rotate(0deg); } &:hover i { transform: rotate(45deg); } } .btn-animate-app { i { transition: all .6s ease; transform: translate3d(0,0,0); transform-style: preserve-3d; } &:hover i { transform: scale(.7) rotateX(-180deg); } } .btn-normalcase { text-transform: none; } ================================================ FILE: client/scss/components/v-data-table.scss ================================================ .v-data-table { .is-clickable { cursor: pointer; } } ================================================ FILE: client/scss/components/v-dialog.scss ================================================ .dialog-header { background-color: mc('blue', '700'); background-image: radial-gradient(ellipse at top, mc('blue', '500'), mc('blue', '700')), radial-gradient(ellipse at bottom, mc('blue', '800'), mc('blue', '700')); height: 60px; color: #FFF; display: flex; align-items: center; padding: 0 1rem; font-size: 1.2rem; &.is-red { background-color: mc('red', '700'); background-image: radial-gradient(ellipse at top, mc('red', '500'), mc('red', '700')), radial-gradient(ellipse at bottom, mc('red', '800'), mc('red', '700')); } &.is-orange { background-color: mc('orange', '700'); background-image: radial-gradient(ellipse at top, mc('orange', '600'), mc('orange', '800')), radial-gradient(ellipse at bottom, mc('orange', '900'), mc('orange', '800')); } &.is-indigo { background-color: mc('indigo', '700'); background-image: radial-gradient(ellipse at top, mc('indigo', '500'), mc('indigo', '700')), radial-gradient(ellipse at bottom, mc('indigo', '800'), mc('indigo', '700')); } &.is-dark { background-color: mc('grey', '900'); background-image: radial-gradient(ellipse at top, mc('grey', '800'), mc('grey', '900')), radial-gradient(ellipse at bottom, mc('grey', '800'), mc('grey', '900')); } &.is-teal { background-color: mc('teal', '700'); background-image: radial-gradient(ellipse at top, mc('teal', '500'), mc('teal', '700')), radial-gradient(ellipse at bottom, mc('teal', '800'), mc('teal', '700')); } } .v-dialog--fullscreen { @include until($tablet) { padding-top: 56px; } } ================================================ FILE: client/scss/components/v-form.scss ================================================ .wiki-form { &.theme--light { background-color: mc('grey', '50'); } .v-text-field--outline { .v-input__slot { background-color: #FFF !important; border-color: mc('grey', '300') !important; border-radius: 7px; @at-root .theme--dark & { background-color: lighten(mc('grey', '900'), 5%) !important; border-color: mc('grey', '700') !important; .v-label.v-label--active.primary--text { color: mc('blue', '500') !important; } } } &.v-input--is-focused .v-input__slot { border-color: mc('blue', '500') !important; } @at-root .theme--dark & { .v-icon.primary--text { color: mc('blue', '500') !important; } } } } ================================================ FILE: client/scss/components/v-tabs.scss ================================================ .grad-tabs > .v-tabs-bar { background-image: linear-gradient(to top, rgba(#000, .025), transparent); border-bottom: 1px solid rgba(#000, .1); border-radius: 4px 4px 0 0; @at-root .theme--dark & { background-image: linear-gradient(to bottom, rgba(#FFF, .05), transparent); border-bottom-color: transparent; } } ================================================ FILE: client/scss/fonts/arabic.scss ================================================ @font-face { font-family: 'Tajawal'; src: url('../../fonts/arabic/Tajawal-Bold.woff2') format('woff2'), url('../../fonts/arabic/Tajawal-Bold.woff') format('woff'); font-weight: bold; font-style: normal; } @font-face { font-family: 'Tajawal'; src: url('../../fonts/arabic/Tajawal-Regular.woff2') format('woff2'), url('../../fonts/arabic/Tajawal-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Tajawal'; src: url('../../fonts/arabic/Tajawal-Medium.woff2') format('woff2'), url('../../fonts/arabic/Tajawal-Medium.woff') format('woff'); font-weight: 500; font-style: normal; } @font-face { font-family: 'BalooBhaijaan'; src: url('../../fonts/arabic/BalooBhaijaan-Regular.woff2') format('woff2'), url('../../fonts/arabic/BalooBhaijaan-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Roboto Mono'; src: url('../../fonts/default/RobotoMono-Regular.woff2') format('woff2'), url('../../fonts/default/RobotoMono-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } html:lang(ar) { font-family: Tajawal, sans-serif; .v-application { font-family: Tajawal, sans-serif; & .headline, & .title { font-family: Tajawal, sans-serif !important; } &.v-application--is-rtl { h1, h2, h3, h4, h5, h6 { font-family: BalooBhaijaan, sans-serif; font-weight: normal; } } } } ================================================ FILE: client/scss/fonts/default.scss ================================================ @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-MediumItalic.woff2') format('woff2'), url('../../fonts/default/Roboto-MediumItalic.woff') format('woff'); font-weight: 500; font-style: italic; } @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-Italic.woff2') format('woff2'), url('../../fonts/default/Roboto-Italic.woff') format('woff'); font-weight: normal; font-style: italic; } @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-Bold.woff2') format('woff2'), url('../../fonts/default/Roboto-Bold.woff') format('woff'); font-weight: bold; font-style: normal; } @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-Regular.woff2') format('woff2'), url('../../fonts/default/Roboto-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-BoldItalic.woff2') format('woff2'), url('../../fonts/default/Roboto-BoldItalic.woff') format('woff'); font-weight: bold; font-style: italic; } @font-face { font-family: 'Roboto'; src: url('../../fonts/default/Roboto-Medium.woff2') format('woff2'), url('../../fonts/default/Roboto-Medium.woff') format('woff'); font-weight: 500; font-style: normal; } @font-face { font-family: 'Roboto Mono'; src: url('../../fonts/default/RobotoMono-Regular.woff2') format('woff2'), url('../../fonts/default/RobotoMono-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } ================================================ FILE: client/scss/global.scss ================================================ @charset "utf-8"; @import "base/material"; @import "base/mixins"; $tablet: 769px !default; $desktop: 980px !default; $widescreen: 1180px !default; $grid-breakpoints: ( 'xs': 0, 'sm': 600px, 'md': 960px, 'lg': 1280px - 16px, 'xl': 1920px - 16px ) !default; $display-breakpoints: ( 'print-only': 'only print', 'screen-only': 'only screen', 'xs-only': 'only screen and (max-width: #{map-get($grid-breakpoints, 'sm') - 1})', 'sm-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')}) and (max-width: #{map-get($grid-breakpoints, 'md') - 1})', 'sm-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'md') - 1})', 'sm-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')})', 'md-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')}) and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})', 'md-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})', 'md-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')})', 'lg-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')}) and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})', 'lg-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})', 'lg-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')})', 'xl-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'xl')})' ) !default; ================================================ FILE: client/scss/layout/_rtl.scss ================================================ .rtl { direction: rtl; .button i { margin-left: 8px; margin-right: 0px; } .nav-right .nav-item { padding: 0 10px 0 0; } .nav-item h1 i { margin-left: 8px; margin-right: 8px; } .sidebar aside .sidebar-menu li a i { margin-left: 7px; margin-right: 0; } .mkcontent { ul { padding: 10px 40px 10px 0; } } } ================================================ FILE: client/scss/legacy.scss ================================================ @import "global"; @import "./base/icons.scss"; @import '~katex/dist/katex.min.css'; @import '~@mdi/font/css/materialdesignicons.css'; .mdi { font-family: 'Material Design Icons', sans-serif; font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ display: inline-flex; line-height: 1; text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; /* Support for Safari and Chrome. */ text-rendering: optimizeLegibility; /* Support for Firefox. */ -moz-osx-font-smoothing: grayscale; /* Support for IE. */ font-feature-settings: 'liga'; } html { box-sizing: border-box; background-color: mc('grey', '50'); font-size: 15px; } *, *:before, *:after { box-sizing: inherit; } * { margin: 0; padding: 0; } .is-hidden { display: none; } body { margin: 0; padding: 0; font-family: "Roboto",sans-serif; line-height: 1.5; min-height: 100vh; } // LOGIN .login { background-color: mc('grey', '900'); height: 100vh; display: flex; align-items: center; justify-content: center; &-deprecated { position: absolute; top: 0; left: 0; width: 100%; background-color: mc('grey', '800'); text-align: center; color: mc('grey', '50'); height: 64px; display: flex; align-items: center; justify-content: center; a { color: mc('red', '200'); margin-left: 5px; } } &-error { background-color: mc('red', '500'); color: #FFF; padding: 5px; border-radius: 5px; margin-bottom: 2rem; } &-dialog { width: 650px; background-color: mc('grey', '100'); border-radius: 5px; text-align: center; padding: 2rem; color: mc('grey', '800'); h1 { margin-bottom: 2rem; } input, select { display: block; background-color: #FFF; border: none; border-radius: 5px; width: 100%; height: 40px; padding: 0 1rem; margin: 5px 0; } button { height: 40px; display: block; width: 200px; border: none; border-radius: 5px; margin: 0 auto; background-color: mc('blue', '700'); color: #FFF; cursor: pointer; margin-top: 1rem; font-weight: 600; &:hover { background-color: mc('blue', '800'); } } } &-social { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid mc('grey', '400'); h2 { font-size: 14px; font-weight: 600; margin-bottom: 1rem; } &-icon { display: inline-flex; justify-content: center; align-items: center; border-radius: 5px; width: 54px; height: 54px; cursor: pointer; transition: opacity .2s ease; margin: .5rem .25rem; &:hover { opacity: .8; } svg { width: 24px; height: 24px; bottom: 0; path { fill: #FFF; } } @each $colorName, $color in $material-colors { &.#{$colorName} { background-color: map-get($color, '500'); } } } } } // PAGE .header { background-color: #000; color: #FFF; height: 64px; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; &-title { margin: 0; font-size: 16px; font-weight: 500; letter-spacing: .02em; } &-deprecated { color: mc('red', '100'); a { color: mc('pink', '400'); } } &-login { a { text-decoration: none; color: #FFF; transition: color .3s ease; border-radius: 50%; background-color: mc('grey', '900'); display: flex; width: 40px; height: 40px; justify-content: center; align-items: center; &:hover { color: mc('blue', '500'); } } } } .main { display: flex; align-items: stretch; min-height: calc(100vh - 64px); height: 100%; &-container { flex-grow: 1; } } .sidebar { width: 256px; background-color: mc('blue', '700'); color: #FFF; padding: 8px 0; align-self: stretch; flex-shrink: 0; .sidebar-link { height: 40px; font-size: 13px; display: flex; align-items: center; padding: 0 16px; transition: background .3s cubic-bezier(.25,.8,.5,1); font-weight: 400; color: #FFF; text-decoration: none; &:hover { background: hsla(0,0%,100%,.08); } } i.mdi { width: 56px; padding-left: 8px; } .sidebar-divider { border-top: 1px solid hsla(0,0%,100%,.12); margin: 8px 0; } .sidebar-title { font-size: 13px; height: 40px; display: flex; align-items: center; padding: 0 16px 0 24px; font-weight: 500; color: hsla(0,0%,100%,.7); } } .page-header { background-color: mc('grey', '100'); padding: 0 24px; height: 90px; display: flex; align-items: center; border-bottom: 1px solid mc('grey', '200'); h1 { font-size: 24px; font-weight: 400; line-height: 32px; color: mc('grey', '800'); } h2 { color: mc('grey', '600'); font-size: 12px; font-weight: 400; } &-left { flex-grow: 1; } &-right { flex: 0 0 308px; padding-left: 16px; &-title { color: mc('grey', '500'); font-size: 12px; } &-author { color: mc('grey', '800'); font-weight: 500; } &-updated { color: mc('grey', '600'); font-size: 12px; } } } .page-contents { display: flex; } .toc { flex: 0 0 348px; background-color: mc('grey', '200'); padding: 4px 0; &-title { font-size: 13px; height: 40px; display: flex; color: mc('blue', '600'); align-items: center; font-weight: 500; padding: 0 16px; } &-tile { text-decoration: none; height: 40px; display: flex; font-size: 13px; align-items: center; padding: 0 16px; color: mc('grey', '800'); transition: background-color .3s ease; &.inset { padding-left: 32px; } &:hover { background-color: rgba(0,0,0,.06); } } &-divider { border-top: 1px solid rgba(0,0,0,.12); margin: 0 0 0 24px; &.inset { margin-left: 40px; } } } @import "../themes/default/scss/app.scss"; .contents { flex-grow: 1; padding: 24px !important; } ================================================ FILE: client/scss/pages/_error.scss ================================================ .app-error { background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '800') 100%); height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: mc('grey', '50'); font-family: Roboto, Arial, sans-serif; img { width: 250px; filter: grayscale(50%) brightness(120%); animation: errorlogo 5s linear infinite; margin-bottom: 3rem; @include until($tablet) { width: 200px; } } @keyframes errorlogo { 0% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg); } 10% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%); } 15% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(0%); } 30% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg); } 32% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(2700deg) invert(100%); } 34% { filter: blur(0) grayscale(100%) brightness(50%) hue-rotate(110deg); } 50% { filter: blur(0) grayscale(100%) brightness(200%) hue-rotate(110deg) sepia(0%); } 55% { filter: blur(0) grayscale(100%) brightness(100%) hue-rotate(110deg) sepia(100%); } 60% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) sepia(0%); } 90% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg); } 95% { filter: blur(5px) grayscale(50%) brightness(200%) hue-rotate(720deg); } 100% { filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%); } } > strong { font-size: 1.5rem; } > span { margin-top: 1rem; } > pre { margin-top: 2rem; code { color: mc('grey', '500'); font-size: .8rem; } } } ================================================ FILE: client/scss/pages/_new.scss ================================================ .newpage { background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('purple', '500') 100%); height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: mc('grey', '50'); &::before { content: ''; display:block; width: 100%; height: 100%; position: absolute; top: 0; left: 0; background-image: url('../static/svg/motif-circuit.svg'); background-position: center center; background-repeat: repeat; background-size: 200px; z-index: 0; opacity: .75; animation: onboardingBgReveal 80s linear infinite; @include keyframes(onboardingBgReveal) { 0% { background-position-y: 0; } 100% { background-position-y: -2000px; } } } &::after { content: ''; position: absolute; background-color: transparent; background-image: url('../static/svg/motif-overlay.svg'); background-attachment: fixed; background-size: cover; opacity: .5; top: 0; left: 0; width: 100vw; height: 100vh; } &-content { display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2; } img { height: 250px; margin-bottom: 3rem; z-index: 2; animation-duration: 2s; @include until($tablet) { height: 200px; } } h1 { font-size: 1.5rem; margin-bottom: 1rem; z-index: 2; } h2 { margin-bottom: 3rem; z-index: 2; } .v-btn { z-index: 2; } } ================================================ FILE: client/scss/pages/_notfound.scss ================================================ .notfound { background: linear-gradient(to bottom, darken(mc('red', '900'), 25%) 0%, mc('red', '600') 100%); height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: mc('grey', '50'); &::before { content: ''; display:block; width: 100%; height: 100%; position: absolute; top: 0; left: 0; background-image: url('../static/svg/motif-circuit.svg'); background-position: center center; background-repeat: repeat; background-size: 200px; z-index: 0; opacity: .75; animation: onboardingBgReveal 80s linear infinite; @include keyframes(onboardingBgReveal) { 0% { background-position-y: 0; } 100% { background-position-y: -2000px; } } } &::after { content: ''; position: absolute; background-color: transparent; background-image: url('../static/svg/motif-overlay.svg'); background-attachment: fixed; background-size: cover; opacity: .5; top: 0; left: 0; width: 100vw; height: 100vh; } &-content { display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2; } img { height: 250px; margin-bottom: 3rem; z-index: 2; animation-duration: 2s; @include until($tablet) { height: 200px; } } h1 { font-size: 1.5rem; margin-bottom: 1rem; z-index: 2; } h2 { margin-bottom: 3rem; z-index: 2; } .v-btn { z-index: 2; } } ================================================ FILE: client/scss/pages/_unauthorized.scss ================================================ .unauthorized { background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('red', '500') 100%); height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: mc('grey', '50'); &::before { content: ''; display:block; width: 100%; height: 100%; position: absolute; top: 0; left: 0; background-image: url('../static/svg/motif-diagonals.svg'); background-position: center center; background-repeat: repeat; background-size: 50px; z-index: 0; opacity: .75; animation: onboardingBgReveal 50s linear infinite; @include keyframes(onboardingBgReveal) { 0% { background-position-y: 0; } 100% { background-position-y: -2000px; } } } &::after { content: ''; position: absolute; background-color: transparent; background-image: url('../static/svg/motif-overlay.svg'); background-attachment: fixed; background-size: cover; opacity: .5; top: 0; left: 0; width: 100vw; height: 100vh; } &-content { display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2; } img { height: 250px; margin-bottom: 3rem; z-index: 2; animation-duration: 2s; @include until($tablet) { height: 200px; } } h1 { font-size: 1.5rem; margin-bottom: 1rem; z-index: 2; } h2 { margin-bottom: 3rem; z-index: 2; } .v-btn { z-index: 2; } } ================================================ FILE: client/scss/pages/_welcome.scss ================================================ .onboarding { background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '700') 100%); height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: mc('grey', '50'); &::before { content: ''; display:block; width: 100%; height: 100%; position: absolute; top: 0; left: 0; background-image: url('../static/svg/motif-blocks.svg'); background-position: center center; background-repeat: repeat; background-size: 500px; z-index: 0; opacity: .75; animation: onboardingBgReveal 50s linear infinite; @include keyframes(onboardingBgReveal) { 0% { background-position-y: 0; } 100% { background-position-y: -2000px; } } } &-content { display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2; } img { width: 500px; filter: grayscale(100%) brightness(160%); margin-bottom: 3rem; z-index: 2; animation-duration: 3s; @include until($tablet) { width: 300px; } } h1 { font-size: 1.5rem; margin-bottom: 1rem; z-index: 2; } h2 { margin-bottom: 3rem; z-index: 2; } .v-btn { z-index: 2; } } ================================================ FILE: client/static/browserconfig.xml ================================================ #1976d2 ================================================ FILE: client/static/favicons/browserconfig.xml ================================================ #1976d2 ================================================ FILE: client/static/manifest.json ================================================ { "name": "Wiki.js", "short_name": "Wiki.js", "start_url": "/", "icons": [ { "src": "/_assets/favicons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/_assets/favicons/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" } ], "theme_color": "#1976d2", "background_color": "#1976d2", "display": "standalone" } ================================================ FILE: client/store/admin.js ================================================ import { make } from 'vuex-pathify' const state = { info: { currentVersion: 'n/a', latestVersion: 'n/a', groupsTotal: 0, pagesTotal: 0, usersTotal: 0 } } export default { namespaced: true, state, mutations: make.mutations(state) } ================================================ FILE: client/store/editor.js ================================================ import { make } from 'vuex-pathify' const state = { editor: '', editorKey: '', content: '', mode: 'create', activeModal: '', activeModalData: null, media: { folderTree: [], currentFolderId: 0, currentFileId: null }, checkoutDateActive: '' } export default { namespaced: true, state, mutations: { ...make.mutations(state), pushMediaFolderTree: (st, folder) => { st.media.folderTree = st.media.folderTree.concat(folder) }, popMediaFolderTree: (st) => { st.media.folderTree = st.media.folderTree.slice(0, -1) } } } ================================================ FILE: client/store/index.js ================================================ import _ from 'lodash' import Vue from 'vue' import Vuex from 'vuex' import pathify from 'vuex-pathify' // eslint-disable-line import/no-duplicates import { make } from 'vuex-pathify' // eslint-disable-line import/no-duplicates import page from './page' import site from './site' import user from './user' /* global WIKI */ Vue.use(Vuex) const state = { loadingStack: [], notification: { message: '', style: 'primary', icon: 'cached', isActive: false } } export default new Vuex.Store({ strict: process.env.NODE_ENV !== 'production', plugins: [ pathify.plugin ], state, getters: { isLoading: state => { return state.loadingStack.length > 0 } }, mutations: { ...make.mutations(state), loadingStart (st, stackName) { st.loadingStack = _.union(st.loadingStack, [stackName]) }, loadingStop (st, stackName) { st.loadingStack = _.without(st.loadingStack, stackName) }, showNotification (st, opts) { st.notification = _.defaults(opts, { message: '', style: 'primary', icon: 'cached', isActive: true }) }, updateNotificationState (st, newState) { st.notification.isActive = newState }, pushGraphError (st, err) { WIKI.$store.commit('showNotification', { style: 'red', message: _.get(err, 'graphQLErrors[0].message', err.message), icon: 'alert' }) } }, actions: { }, modules: { page, site, user } }) ================================================ FILE: client/store/page.js ================================================ import { make } from 'vuex-pathify' const state = { id: 0, authorId: 0, authorName: 'Unknown', createdAt: '', description: '', isPublished: true, locale: 'en', path: '', publishEndDate: '', publishStartDate: '', tags: [], title: '', updatedAt: '', editor: '', mode: '', scriptJs: '', scriptCss: '', effectivePermissions: { comments: { read: false, write: false, manage: false }, history: { read: false }, source: { read: false }, pages: { write: false, manage: false, delete: false, script: false, style: false }, system: { manage: false } }, commentsCount: 0, editShortcuts: { editFab: false, editMenuBar: false, editMenuBtn: false, editMenuExternalBtn: false, editMenuExternalName: '', editMenuExternalIcon: '', editMenuExternalUrl: '' } } export default { namespaced: true, state, mutations: make.mutations(state) } ================================================ FILE: client/store/site.js ================================================ import { make } from 'vuex-pathify' /* global siteConfig */ const state = { company: siteConfig.company, contentLicense: siteConfig.contentLicense, footerOverride: siteConfig.footerOverride, dark: siteConfig.darkMode, tocPosition: siteConfig.tocPosition, mascot: true, title: siteConfig.title, logoUrl: siteConfig.logoUrl, search: '', searchIsFocused: false, searchIsLoading: false, searchRestrictLocale: false, searchRestrictPath: false, printView: false } export default { namespaced: true, state, mutations: make.mutations(state) } ================================================ FILE: client/store/user.js ================================================ import { make } from 'vuex-pathify' import jwt from 'jsonwebtoken' import Cookies from 'js-cookie' const state = { id: 0, email: '', name: '', pictureUrl: '', localeCode: '', defaultEditor: '', timezone: '', dateFormat: '', appearance: '', permissions: [], iat: 0, exp: 0, authenticated: false } export default { namespaced: true, state, mutations: { ...make.mutations(state), REFRESH_AUTH(st) { const jwtCookie = Cookies.get('jwt') if (jwtCookie) { try { const jwtData = jwt.decode(jwtCookie) st.id = jwtData.id st.email = jwtData.email st.name = jwtData.name st.pictureUrl = jwtData.av st.localeCode = jwtData.lc st.timezone = jwtData.tz || Intl.DateTimeFormat().resolvedOptions().timeZone || '' st.dateFormat = jwtData.df || '' st.appearance = jwtData.ap || '' // st.defaultEditor = jwtData.defaultEditor st.permissions = jwtData.permissions st.iat = jwtData.iat st.exp = jwtData.exp st.authenticated = true } catch (err) { console.debug('Invalid JWT. Silent authentication skipped.') } } } } } ================================================ FILE: client/themes/default/components/nav-footer.vue ================================================ ================================================ FILE: client/themes/default/components/nav-sidebar.vue ================================================ ================================================ FILE: client/themes/default/components/page.vue ================================================ ================================================ FILE: client/themes/default/components/tabset.vue ================================================ ================================================ FILE: client/themes/default/js/app.js ================================================ /* THEME SPECIFIC JAVASCRIPT */ ================================================ FILE: client/themes/default/scss/app.scss ================================================ /* THEME SPECIFIC STYLES */ .v-main .contents { color: mc('grey', '800'); padding: .5rem 0 50px; position: relative; > div > *:first-child { margin-top: 0; } @at-root .theme--dark & { color: mc('grey', '300'); } // --------------------------------- // LINKS // --------------------------------- a { color: mc('blue', '700'); &.is-internal-link.is-invalid-page { color: mc('red', '700'); @at-root .theme--dark & { color: mc('red', '200'); } } &.is-external-link { padding-right: 3px; &::after { font-family: 'Material Design Icons', sans-serif; font-size: 24px/1; padding-left: 3px; display: inline-block; content: '\F03CC'; color: mc('grey', '500'); text-decoration: none; } } @at-root .theme--dark & { color: mc('blue', '200'); } } // --------------------------------- // HEADERS // --------------------------------- h1, h2, h3, h4, h5, h6 { position: relative; &:first-child { padding-top: 0; } &:hover { .toc-anchor { display: block; } } .toc-anchor { display: none; position: absolute; right: 1rem; bottom: .5rem; font-size: 1.25rem; text-decoration: none; color: mc('grey', '500'); } & + h2, & + h3, & + h4, & + h5, & + h6 { margin-top: 8px; } & + hr.footnotes-sep { display: none; } } h1 { padding: 0; color: mc('blue', '800'); margin-top: 2rem; position: relative; @at-root .theme--dark & { color: mc('grey', '300'); } &::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: linear-gradient(to right, mc('theme', 'primary'), rgba(mc('theme', 'primary'), 0)); border-radius: 3px; @at-root .theme--dark & { background: linear-gradient(to right, mc('blue', '300') 0%, mc('blue', '500') 10%, rgba(mc('blue', '900'), 0) 100%); } @at-root .is-rtl & { background: linear-gradient(to left, mc('theme', 'primary'), rgba(mc('theme', 'primary'), 0)); } @at-root .theme--dark.is-rtl & { background: linear-gradient(to left, mc('grey', '600'), rgba(mc('grey', '600'), 0)); } } } h2 { margin: 1rem 0 0 0; color: mc('grey', '800'); position: relative; @at-root .theme--dark & { color: mc('grey', '400'); } &::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, mc('grey', '700'), rgba(mc('grey', '700'), 0)); @at-root .theme--dark & { background: linear-gradient(to right, mc('grey', '300'), rgba(mc('grey', '700'), 0)); } @at-root .is-rtl & { background: linear-gradient(to left, mc('grey', '700'), rgba(mc('grey', '700'), 0)); } @at-root .theme--dark.is-rtl & { background: linear-gradient(to left, mc('grey', '300'), rgba(mc('grey', '700'), 0)); } } } h3 { margin: 8px 0 0 0; color: mc('grey', '700'); position: relative; @at-root .theme--dark & { color: mc('grey', '600'); } &::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 90%); } } h4, h5, h6 { font-size: 1rem; margin: 8px 0 0 0; color: mc('grey', '700'); position: relative; @at-root .theme--dark & { color: mc('grey', '600'); } &::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 70%); } } h5 { &::after { background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 50%); } } h6 { &::after { background: linear-gradient(to right, mc('grey', '500'), rgba(mc('grey', '500'), 0) 30%); } } // --------------------------------- // PARAGRAPHS // --------------------------------- p { padding: 1rem 0 0 0; margin: 0; @at-root .contents > div > p:first-child { padding-top: 0; } @at-root .v-application & { margin-bottom: 0; } } hr { margin: 1rem 0; height: 1px; border: none; background-color: mc('grey', '400'); @at-root .theme--dark & { background-color: mc('grey', '700'); } } .emoji { height: 1.25em; margin: 0 1px -4px; } .text-huge { font-size: 1.8em; } .text-big { font-size: 1.4em; } .text-small { font-size: .85em; } .text-tiny { font-size: .7em; } blockquote { padding: 0 1rem 1rem 1rem; background-color: mc('blue-grey', '50'); border-left: 55px solid mc('blue-grey', '500'); border-radius: .5rem; margin: 1rem 0; position: relative; @at-root .theme--dark & { background-color: mc('blue-grey', '900'); } &::before { display: inline-block; font: normal normal normal 24px/1 "Material Design Icons", sans-serif; position: absolute; margin-top: -12px; top: 50%; left: -38px; color: rgba(255, 255, 255, .7); content: "\F0757"; } > p:first-child .emoji { margin-right: .5rem; } &.valign-center > p { display: flex; align-items: center; } &.is-info { background-color: mc('blue', '50'); border-color: mc('blue', '300'); color: mc('blue', '900'); &::before { content: "\F02FC"; } code:not([class^="language-"]) { background-color: mc('blue', '50'); color: mc('blue', '800'); } @at-root .theme--dark & { background-color: mc('blue', '900'); color: mc('blue', '50'); border-color: mc('blue', '500'); } } &.is-warning { background-color: mc('orange', '50'); border-color: mc('orange', '300'); color: darken(mc('orange', '900'), 10%); &::before { content: "\F0026"; } code:not([class^="language-"]) { background-color: mc('orange', '50'); color: mc('orange', '800'); } @at-root .theme--dark & { background-color: darken(mc('orange', '900'), 5%); color: mc('orange', '100'); border-color: mc('orange', '500'); box-shadow: 0 0 2px 0 mc('grey', '900'); } } &.is-danger { background-color: mc('red', '50'); border-color: mc('red', '300'); color: mc('red', '900'); &::before { content: "\F0159"; } code:not([class^="language-"]) { background-color: mc('red', '50'); color: mc('red', '800'); } @at-root .theme--dark & { background-color: mc('red', '900'); color: mc('red', '100'); border-color: mc('red', '500'); } } &.is-success { background-color: mc('green', '50'); border-color: mc('green', '300'); color: mc('green', '900'); &::before { content: "\F0E1E"; } code:not([class^="language-"]) { background-color: mc('green', '50'); color: mc('green', '800'); } @at-root .theme--dark & { background-color: mc('green', '900'); color: mc('green', '50'); border-color: mc('green', '500'); } } } // --------------------------------- // ASCIIDOC SPECIFIC // --------------------------------- .admonitionblock { margin: 1rem 0; position: relative; table { border: none; background-color: transparent; width: 100%; } td.icon { border-bottom-left-radius: 7px; border-top-left-radius: 7px; text-align: center; width: 56px; &::before { display: inline-block; font: normal normal normal 24px/1 "Material Design Icons", sans-serif !important; } } td.content { border-bottom-right-radius: 7px; border-top-right-radius: 7px; } &.note { td.icon { background-color: mc('blue', '300'); color: mc('blue', '50'); &::before { content: "\F02FC"; } } td.content { color: darken(mc('blue', '900'), 10%); background-color: mc('blue', '50'); @at-root .theme--dark & { background-color: mc('blue', '900'); color: mc('blue', '50'); } } } &.tip { td.icon { background-color: mc('green', '300'); color: mc('green', '50'); &::before { content: "\F0335"; } } td.content { color: darken(mc('green', '900'), 10%); background-color: mc('green', '50'); @at-root .theme--dark & { background-color: mc('green', '900'); color: mc('green', '50'); } } } &.warning { background-color: transparent !important; td.icon { background-color: mc('orange', '300'); color: #FFF; &::before { content: "\F0026"; } } td.content { color: darken(mc('orange', '900'), 10%); background-color: mc('orange', '50'); @at-root .theme--dark & { background-color: darken(mc('orange', '900'), 5%); color: mc('orange', '100'); } } } &.caution { td.icon { background-color: mc('purple', '300'); color: mc('purple', '50'); &::before { content: "\f0238"; } } td.content { color: darken(mc('purple', '900'), 10%); background-color: mc('purple', '50'); @at-root .theme--dark & { background-color: mc('purple', '900'); color: mc('purple', '100'); } } } &.important { td.icon { background-color: mc('red', '300'); color: mc('red', '50'); &::before { content: "\F0159"; } } td.content { color: darken(mc('red', '900'), 10%); background-color: mc('red', '50'); @at-root .theme--dark & { background-color: mc('red', '900'); color: mc('red', '100'); } } } } .exampleblock { > .title { font-style: italic; font-size: 1rem !important; color: #7a2717; @at-root .theme--dark & { color: mc('brown', '300'); } } > .content { border: 1px solid mc('grey', '200'); border-radius: 7px; margin-bottom: 12px; padding: 16px; } } // --------------------------------- // LISTS // --------------------------------- ol, ul:not(.tabset-tabs) { padding-top: 1rem; width: 100%; @at-root .is-rtl & { padding-left: 0; padding-right: 1rem; } li > ul, li > ol { padding-top: .5rem; padding-left: 1rem; @at-root .is-rtl & { padding-left: 0; padding-right: 1rem; } } li + li { margin-top: .5rem; } &.links-list { padding-left: 0; list-style-type: none; @at-root .is-rtl & { padding-right: 0; } li { background-color: mc('grey', '50'); background-image: linear-gradient(to bottom, #FFF, mc('grey', '50')); border-right: 1px solid mc('grey', '200'); border-bottom: 1px solid mc('grey', '200'); border-left: 5px solid mc('grey', '300'); box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); padding: 1rem; border-radius: 5px; font-weight: 500; @at-root .is-rtl & { border-left-width: 1px; border-right-width: 5px; } &:hover { background-image: linear-gradient(to bottom, #FFF, lighten(mc('blue', '50'), 4%)); border-left-color: mc('blue', '500'); cursor: pointer; @at-root .is-rtl & { border-left-color: mc('grey', '200'); border-right-width: mc('blue', '500'); } } &::before { content: ''; display: none; } > a { display: block; text-decoration: none; margin: -1rem; padding: 1rem; > em { font-weight: 400; font-style: normal; color: mc('grey', '700'); display: inline-block; padding-left: .5rem; border-left: 1px solid mc('grey', '300'); margin-left: .5rem; &.is-block { display: block; padding-left: 0; margin-left: 0; border-left: none; } } } > em { font-weight: 400; font-style: normal; } @at-root .theme--dark & { background-color: mc('grey', '50'); background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 5%), mc('grey', '900')); border-right: 1px solid mc('grey', '900'); border-bottom: 1px solid mc('grey', '900'); border-left: 5px solid mc('grey', '700'); box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.1); @at-root .theme--dark.is-rtl & { border-left-width: 1px; border-right-width: 5px; } &:hover { background-image: linear-gradient(to bottom, lighten(mc('grey', '900'), 2%), darken(mc('grey', '900'), 3%)); border-left-color: mc('indigo', '300'); cursor: pointer; @at-root .theme--dark.is-rtl & { border-left-color: mc('grey', '900'); border-right-width: mc('indigo', '300'); } } } } } &.grid-list { margin: 1rem 0 0 0; background-color: #FFF; border: 1px solid mc('grey', '200'); padding: 1px; display: inline-block; list-style-type: none; @at-root .theme--dark & { background-color: #000; border: 1px solid mc('grey', '800'); } li { background-color: mc('grey', '50'); padding: .6rem 1rem; display: block; &:nth-child(odd) { background-color: mc('grey', '100'); } & + li { margin-top: 0; } &::before { content: ''; display: none; } @at-root .theme--dark & { background-color: mc('grey', '900'); &:nth-child(odd) { background-color: darken(mc('grey', '900'), 5%); } } } } } ul:not(.tabset-tabs):not(.contains-task-list) { list-style: none; > li::before { position: absolute; left: -1.1rem; content: '\25b8'; color: mc('grey', '600'); width: 1.35rem; @at-root .is-rtl & { right: -1.1rem; content: '\25C3'; } } } ol, ul:not(.tabset-tabs) { > li { position: relative; > p { display:inline-block; vertical-align:top; padding-top:0; &:first-child { width: 100%; } } } } dl { dt { margin-top: 0.3em; margin-bottom: 0.3em; font-weight: bold; } dd { margin-left: 1.125em; margin-bottom: 0.75em; > p { padding: 0; } } } // --------------------------------- // CODE // --------------------------------- code { background-color: mc('indigo', '50'); padding: 0 5px; color: mc('indigo', '800'); font-family: 'Roboto Mono', monospace; font-weight: normal; font-size: 1rem; box-shadow: none; &::before, &::after { display: none; } @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 5%); color: mc('indigo', '100'); } } .prismjs{ border: none; border-radius: 5px; box-shadow: initial; background-color: mc('grey', '900'); padding: 1rem 1rem 1rem 3rem; margin: 1rem 0; @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 5%); } > code { background-color: transparent; padding: 0; color: #FFF; box-shadow: initial; display: block; font-size: .85rem; font-family: 'Roboto Mono', monospace; &:after, &:before { content: initial; letter-spacing: initial; } } } .diagram { margin-top: 1rem; overflow: auto; svg { color-scheme: light !important; &:first-child { direction: ltr; } @at-root .theme--dark & { color-scheme: dark !important; } } } // --------------------------------- // TASK LISTS // --------------------------------- .contains-task-list { padding-left: 0; } .task-list-item { position: relative; list-style-type: none; &-checkbox[disabled] { width: 1.1rem; height: 1.1rem; top: 2px; position: relative; margin-right: 2px; &::after { position: absolute; left: 0; top: 0; content: ' '; display: block; width: 1.1rem; height: 1.1rem; background-color: #FFF; border: 1px solid mc('grey', '400'); border-radius: 2px; font-weight: bold; font-size: .8rem; line-height: 1rem; text-align: center; @at-root .theme--dark & { background-color: mc('grey', '900'); border-color: mc('grey', '700'); } } &[checked]::after { content: '✓'; } } .contains-task-list { padding: .5rem 0 0 1.5rem; } } // --------------------------------- // TABLES // --------------------------------- table { margin: .5rem 0; border-spacing: 0; border-radius: 5px; border: 1px solid mc('grey', '300'); @at-root .theme--dark & { border-color: mc('grey', '600'); } &.dense { td, th { font-size: .85rem; padding: .5rem; } } th { padding: .75rem; border-bottom: 2px solid mc('grey', '500'); color: mc('grey', '600'); background-color: mc('grey', '100'); @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 8%); border-bottom-color: mc('grey', '600'); color: mc('grey', '500'); } &:first-child { border-top-left-radius: 7px; } &:last-child { border-top-right-radius: 7px; } } td { padding: .75rem; } tr { td { border-bottom: 1px solid mc('grey', '300'); border-right: 1px solid mc('grey', '100'); @at-root .theme--dark & { border-bottom-color: mc('grey', '700'); border-right-color: mc('grey', '800'); } &:nth-child(even) { background-color: mc('grey', '50'); @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 4%); } } &:last-child { border-right: none; } } &:nth-child(even) { td { background-color: mc('grey', '50'); @at-root .theme--dark & { background-color: darken(mc('grey', '800'), 8%); } &:nth-child(even) { background-color: mc('grey', '100'); @at-root .theme--dark & { background-color: darken(mc('grey', '800'), 10%); } } } } &:last-child { td { border-bottom: none; &:first-child { border-bottom-left-radius: 7px; } &:last-child { border-bottom-right-radius: 7px; } } } } } figure.table { margin: 0; > table { background-color: #FFF; margin: 0; border-collapse: collapse; box-shadow: 0 0 5px 0 rgba(0, 0, 0, .07); @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 3%); } td, th { border: 1px solid mc('blue-grey', '100'); box-shadow: inset -1px -1px 0 0 #FFF, inset 1px 0 0 #FFF; padding: .5rem .75rem; border-radius: 0 !important; @at-root .theme--dark & { border-color: mc('grey', '700'); box-shadow: inset -1px -1px 0 0 rgba(0,0,0, .5); } } th { background-color: lighten(mc('blue-grey', '50'), 1%); font-weight: 700; color: mc('blue-grey', '700'); @at-root .theme--dark & { background-color: mc('grey', '800'); color: mc('grey', '400'); } } thead th { border-bottom: 2px solid mc('blue-grey', '100'); @at-root .theme--dark & { border-bottom: none; } } tbody th { background-color: lighten(mc('blue-grey', '50'), 4%); @at-root .theme--dark & { background-color: darken(mc('grey', '800'), 8%); } } } } // -> Add horizontal scrollbar when table is too wide .table-container { width: 100%; overflow-x: auto; } // --------------------------------- // IMAGES // --------------------------------- img { max-width: 100%; &.align-left { float: left; margin: 0 1rem 1rem 0; } &.align-right { float: right; margin: 0 0 1rem 1rem; z-index: 1; position: relative; } &.align-center { display: block; max-width: 100%; margin: auto; } &.align-abstopright { position: absolute; top: -90px; right: 1rem; height: calc(90px - 32px); width: auto; @at-root .is-rtl & { left: 1rem; right: initial; } } &.decor-shadow { box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); } &.decor-outline { border: 1px solid mc('grey', '400'); } &.uml-diagram { margin: 1rem 0; } } figure.image { margin: 1rem 0 0 0; img { margin: 0 auto; } figcaption { padding: 4px 1rem; text-align: center; font-size: 12px; color: mc('grey', '700'); background-color: mc('grey', '100'); @at-root .theme--dark & { color: mc('grey', '400'); background-color: mc('grey', '800'); } } } figure.image-style-align-right { float: right; } figure.image-style-align-left { float: left; } // --------------------------------- // DETAILS // --------------------------------- details { background-color: mc('grey', '50'); margin: 1rem 2rem; border: 1px solid mc('grey', '300'); border-radius: 7px; > p { padding-left: 0; } > summary { border-radius: 7px; background-color: mc('grey', '50'); cursor: pointer; display: list-item; align-items: center; padding: 0.4rem 1rem; transition: background-color .4s ease; &:focus { outline: none; background-color: mc('grey', '100'); } > h1, h2, h3, h4, h5, h6 { width: 95%; display: inline-block; &:first-child { margin-top: 0; } &:only-child::after { display: none; } } } &[open] { padding: 1rem; > summary { background-color: mc('grey', '100'); border-bottom: 1px solid mc('grey', '300'); border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin: -1rem -1rem 1rem -1rem; } } @at-root .theme--dark & { background-color: mc('grey', '900'); border-color: mc('grey', '700'); > summary { background-color: mc('grey', '900'); border-color: mc('grey', '700'); } &[open] > summary { background-color: lighten(mc('grey', '900'), 5%); } } } // --------------------------------- // HIGHLIGHTING // --------------------------------- mark { &.pen-red { color: mc('red', '500'); background-color: initial; } &.pen-green { color: mc('green', '500'); background-color: initial; } &.marker-blue { background-color: mc('blue', '300'); } &.marker-yellow { background-color: mc('yellow', '300'); } &.marker-pink { background-color: mc('pink', '300'); } &.marker-green { background-color: mc('green', '300'); } } .mention { background-color: rgba(153, 0, 48, .1); color: #990030; @at-root .theme--dark & { color: mc('pink', '500'); } } } // --------------------------------- // COMMENTS // --------------------------------- .comments { &-container { border-radius: 7px; } &-header { color: #FFF; padding: 8px 20px; font-size: 16px; font-weight: 500; background-color: mc('blue-grey', '500'); border-radius: 7px 7px 0 0; @at-root .theme--dark & { background-color: lighten(mc('blue-grey', '900'), 5%); } } &-main { background-color: mc('blue-grey', '50'); border-radius: 0 0 7px 7px; padding: 20px; @at-root .theme--dark & { background-color: darken(mc('grey', '900'), 5%); } } } // --------------- // RTL FIXES // Vuetify GH Issue: https://github.com/vuetifyjs/vuetify/issues/6317 // --------------- .is-rtl { .page-col-content.is-page-header { @each $size, $width in $grid-breakpoints { @media (min-width: $width) { @for $n from 0 through 12 { &.offset-#{$size}-#{$n} { margin-left: 0; margin-right: ($n / 12 * 100) * 1%; } } } } } } // --------------- // PRINT OVERRIDES // --------------- @media print { .nav-header, .v-navigation-drawer, .v-btn--fab, .page-col-sd, .v-tooltip__content { display: none !important; } .layout { display: block !important; } .page-col-content { flex-basis: 100% !important; flex-grow: 1 !important; max-width: 100% !important; margin-left: 0 !important; > .v-toolbar { border: 1px solid mc('grey', '300') !important; border-radius: 7px !important; & + .v-divider { display: none !important; } } } .v-main { padding: 0 !important; font-size: 14px; background-color: #FFF; } .v-main .contents { color: #000; background-color: #FFF; @at-root .theme--dark & { color: #000; } .prismjs{ box-shadow: none; background-color: #FFF; @at-root .theme--dark & { background-color: #FFF; } > code { color: #000; box-shadow: none; text-shadow: none; white-space: pre-wrap !important; overflow-wrap: break-word !important; } } } .comments-container { display: none; } .page-edit-shortcuts { display: none; } } ================================================ FILE: client/themes/default/theme.yml ================================================ name: Default author: requarks.io site: https://wiki.requarks.io/ version: 1.0.0 requirements: minimum: '>= 2.0.0' maximum: '< 3.0.0' props: sdPosition: type: String default: 'left' title: Table of Contents Position hint: Should the content sidebar be shown on the left or right. enum: - 'hidden' - 'left' - 'right' order: 1 icon: mdi-border-vertical showTOC: type: Boolean default: true title: Display the Table of Contents order: 2 showTags: type: Boolean default: true title: Display the Page Tags order: 3 showTags: type: Boolean default: true title: Display the Page Author and Date order: 4 showTags: type: Boolean default: true title: Display the Page Rating order: 5 showSocialBar: type: Boolean default: true title: Display the Social Links Bar order: 6 showEditSpeedDial: type: Boolean default: true title: Display the Edit Speed Dial hint: Shown in the lower right corner of the page. order: 7 ================================================ FILE: config.sample.yml ================================================ ####################################################################### # Wiki.js - CONFIGURATION # ####################################################################### # Full documentation + examples: # https://docs.requarks.io/install # --------------------------------------------------------------------- # Port the server should listen to # --------------------------------------------------------------------- port: 3000 # --------------------------------------------------------------------- # Database # --------------------------------------------------------------------- # Supported Database Engines: # - postgres = PostgreSQL 9.5 or later # - mysql = MySQL 8.0 or later (5.7.8 partially supported, refer to docs) # - mariadb = MariaDB 10.2.7 or later # - mssql = MS SQL Server 2012 or later # - sqlite = SQLite 3.9 or later db: type: postgres # PostgreSQL / MySQL / MariaDB / MS SQL Server only: host: localhost port: 5432 user: wikijs pass: wikijsrocks db: wiki ssl: false # Optional - PostgreSQL / MySQL / MariaDB only: # -> Uncomment lines you need below and set `auto` to false # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options sslOptions: auto: true # rejectUnauthorized: false # ca: path/to/ca.crt # cert: path/to/cert.crt # key: path/to/key.pem # pfx: path/to/cert.pfx # passphrase: xyz123 # Optional - PostgreSQL only: schema: public # SQLite only: storage: path/to/database.sqlite ####################################################################### # ADVANCED OPTIONS # ####################################################################### # Do not change unless you know what you are doing! # --------------------------------------------------------------------- # SSL/TLS Settings # --------------------------------------------------------------------- # Consider using a reverse proxy (e.g. nginx) if you require more # advanced options than those provided below. ssl: enabled: false port: 3443 # Provider to use, possible values: custom, letsencrypt provider: custom # ++++++ For custom only ++++++ # Certificate format, either 'pem' or 'pfx': format: pem # Using PEM format: key: path/to/key.pem cert: path/to/cert.pem # Using PFX format: pfx: path/to/cert.pfx # Passphrase when using encrypted PEM / PFX keys (default: null): passphrase: null # Diffie Hellman parameters, with key length being greater or equal # to 1024 bits (default: null): dhparam: null # ++++++ For letsencrypt only ++++++ domain: wiki.yourdomain.com subscriberEmail: admin@example.com # --------------------------------------------------------------------- # Database Pool Options # --------------------------------------------------------------------- # Refer to https://github.com/vincit/tarn.js for all possible options pool: # min: 2 # max: 10 # --------------------------------------------------------------------- # IP address the server should listen to # --------------------------------------------------------------------- # Leave 0.0.0.0 for all interfaces bindIP: 0.0.0.0 # --------------------------------------------------------------------- # Log Level # --------------------------------------------------------------------- # Possible values: error, warn, info (default), verbose, debug, silly logLevel: info # --------------------------------------------------------------------- # Log Format # --------------------------------------------------------------------- # Output format for logging, possible values: default, json logFormat: default # --------------------------------------------------------------------- # Offline Mode # --------------------------------------------------------------------- # If your server cannot access the internet. Set to true and manually # download the offline files for sideloading. offline: false # --------------------------------------------------------------------- # High-Availability # --------------------------------------------------------------------- # Set to true if you have multiple concurrent instances running off the # same DB (e.g. Kubernetes pods / load balanced instances). Leave false # otherwise. You MUST be using PostgreSQL to use this feature. ha: false # --------------------------------------------------------------------- # Data Path # --------------------------------------------------------------------- # Writeable data path used for cache and temporary user uploads. dataPath: ./data # --------------------------------------------------------------------- # Body Parser Limit # --------------------------------------------------------------------- # Maximum size of API requests body that can be parsed. Does not affect # file uploads. bodyParserLimit: 5mb ================================================ FILE: cypress.json ================================================ { "baseUrl": "http://localhost:3000", "projectId": "r7qxah", "fixturesFolder": false, "integrationFolder": "dev/cypress/integration", "pluginsFile": "dev/cypress/plugins/index.js", "screenshotsFolder": "dev/cypress/screenshots", "supportFile": "dev/cypress/support/index.js", "videosFolder": "dev/cypress/videos", "numTestsKeptInMemory": 1 } ================================================ FILE: dev/build/Dockerfile ================================================ # ==================== # --- Build Assets --- # ==================== FROM node:24-alpine AS assets RUN apk add yarn g++ make cmake python3 --no-cache WORKDIR /wiki COPY ./client ./client COPY ./dev ./dev COPY ./patches ./patches COPY ./package.json ./package.json COPY ./.babelrc ./.babelrc COPY ./.eslintignore ./.eslintignore COPY ./.eslintrc.yml ./.eslintrc.yml RUN yarn cache clean RUN yarn --frozen-lockfile --non-interactive RUN yarn build RUN rm -rf /wiki/node_modules RUN yarn --production --frozen-lockfile --non-interactive RUN yarn patch-package # =============== # --- Release --- # =============== FROM node:24-alpine LABEL maintainer="requarks.io" RUN apk add bash curl git openssh gnupg sqlite --no-cache && \ mkdir -p /wiki && \ mkdir -p /logs && \ mkdir -p /wiki/data/content && \ chown -R node:node /wiki /logs WORKDIR /wiki COPY --chown=node:node --from=assets /wiki/assets ./assets COPY --chown=node:node --from=assets /wiki/node_modules ./node_modules COPY --chown=node:node ./server ./server COPY --chown=node:node --from=assets /wiki/server/views ./server/views COPY --chown=node:node ./dev/build/config.yml ./config.yml COPY --chown=node:node ./package.json ./package.json COPY --chown=node:node ./LICENSE ./LICENSE USER node VOLUME ["/wiki/data/content"] EXPOSE 3000 EXPOSE 3443 # HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://localhost:3000/healthz CMD ["node", "--no-deprecation", "server"] ================================================ FILE: dev/build/config.yml ================================================ port: 3000 bindIP: 0.0.0.0 db: type: $(DB_TYPE) host: '$(DB_HOST)' port: $(DB_PORT) user: '$(DB_USER)' pass: '$(DB_PASS)' db: $(DB_NAME) storage: $(DB_FILEPATH) ssl: $(DB_SSL) ssl: enabled: $(SSL_ACTIVE) port: 3443 provider: letsencrypt domain: $(LETSENCRYPT_DOMAIN) subscriberEmail: $(LETSENCRYPT_EMAIL) logLevel: $(LOG_LEVEL:info) logFormat: $(LOG_FORMAT:default) ha: $(HA_ACTIVE) ================================================ FILE: dev/build-arm/Dockerfile ================================================ # ========================= # --- BUILD NPM MODULES --- # ========================= FROM node:20-alpine AS build RUN apk add yarn g++ make cmake python3 --no-cache WORKDIR /wiki COPY ./package.json ./package.json COPY ./patches ./patches RUN yarn --production --frozen-lockfile --non-interactive --network-timeout 100000 RUN yarn patch-package # =============== # --- Release --- # =============== FROM node:20-alpine LABEL maintainer="requarks.io" RUN apk add bash curl git openssh gnupg sqlite --no-cache && \ mkdir -p /wiki && \ mkdir -p /logs && \ mkdir -p /wiki/data/content && \ chown -R node:node /wiki /logs WORKDIR /wiki COPY --chown=node:node ./build/assets ./assets COPY --chown=node:node --from=build /wiki/node_modules ./node_modules COPY --chown=node:node ./server ./server COPY --chown=node:node ./build/server/views ./server/views COPY --chown=node:node ./dev/build/config.yml ./config.yml COPY --chown=node:node ./build/package.json ./package.json COPY --chown=node:node ./LICENSE ./LICENSE USER node VOLUME ["/wiki/data/content"] EXPOSE 3000 EXPOSE 3443 CMD ["node", "server"] ================================================ FILE: dev/containers/Dockerfile ================================================ # -- DEV DOCKERFILE -- # -- DO NOT USE IN PRODUCTION! -- FROM node:24 LABEL maintainer "requarks.io" RUN apt-get update && \ apt-get install -y bash curl git python3 make g++ nano openssh-server gnupg && \ mkdir -p /wiki WORKDIR /wiki ENV dockerdev 1 ENV DEVDB postgres EXPOSE 3000 CMD ["tail", "-f", "/dev/null"] ================================================ FILE: dev/containers/config.yml ================================================ port: 3000 bindIP: 0.0.0.0 db: type: postgres host: db port: 5432 user: wikijs pass: wikijsrocks db: wiki logLevel: info ================================================ FILE: dev/containers/docker-compose.yml ================================================ # -- DEV DOCKER-COMPOSE -- # -- DO NOT USE IN PRODUCTION! -- version: "3" services: db: container_name: wiki-db image: postgres:17-alpine environment: POSTGRES_DB: wiki POSTGRES_PASSWORD: wikijsrocks POSTGRES_USER: wikijs logging: driver: "none" volumes: - db-data:/var/lib/postgresql/data ports: - "15432:5432" adminer: container_name: wiki-adminer image: adminer:latest logging: driver: "none" ports: - "3001:8080" # solr: # container_name: wiki-solr # image: solr:7-alpine # logging: # driver: "none" # ports: # - "8983:8983" # volumes: # - solr-data:/opt/solr/server/solr/mycores # entrypoint: # - docker-entrypoint.sh # - solr-precreate # - wiki wiki: container_name: wiki-app build: context: ../.. dockerfile: dev/containers/Dockerfile depends_on: - db ports: - "3000:3000" volumes: - ../..:/wiki - /wiki/node_modules - /wiki/.git volumes: db-data: # solr-data: ================================================ FILE: dev/cypress/ci-setup.sh ================================================ case $MATRIXENV in postgres) echo "Using PostgreSQL..." docker run -d -p 5432:5432 --name db --network="host" -e "POSTGRES_PASSWORD=Password123!" -e "POSTGRES_USER=wiki" -e "POSTGRES_DB=wiki" postgres:11 while ! docker exec db psql -U wiki -d wiki -c "SELECT 1" &> /dev/null ; do echo "Waiting for database connection..." sleep 2 done docker run -d -p 3000:3000 --name wiki --network="host" -e "DB_TYPE=postgres" -e "DB_HOST=localhost" -e "DB_PORT=5432" -e "DB_NAME=wiki" -e "DB_USER=wiki" -e "DB_PASS=Password123!" requarks/wiki:canary-$REL_VERSION_STRICT ;; mysql) echo "Using MySQL..." docker run -d -p 3306:3306 --name db --network="host" -e "MYSQL_ROOT_PASSWORD=Password123!" -e "MYSQL_USER=wiki" -e "MYSQL_PASSWORD=Password123!" -e "MYSQL_DATABASE=wiki" mysql:8 while ! docker exec db mysql --user=root --password=Password123! -e "SELECT 1" &> /dev/null ; do echo "Waiting for database connection..." sleep 2 done docker run -d -p 3000:3000 --name wiki --network="host" -e "DB_TYPE=mysql" -e "DB_HOST=localhost" -e "DB_PORT=3306" -e "DB_NAME=wiki" -e "DB_USER=wiki" -e "DB_PASS=Password123!" requarks/wiki:canary-$REL_VERSION_STRICT ;; mariadb) echo "Using MariaDB..." docker run -d -p 3306:3306 --name db --network="host" -e "MYSQL_ROOT_PASSWORD=Password123!" -e "MYSQL_USER=wiki" -e "MYSQL_PASSWORD=Password123!" -e "MYSQL_DATABASE=wiki" mariadb:10 while ! docker exec db mysql --user=root --password=Password123! -e "SELECT 1" &> /dev/null ; do echo "Waiting for database connection..." sleep 2 done docker run -d -p 3000:3000 --name wiki --network="host" -e "DB_TYPE=mariadb" -e "DB_HOST=localhost" -e "DB_PORT=3306" -e "DB_NAME=wiki" -e "DB_USER=wiki" -e "DB_PASS=Password123!" requarks/wiki:canary-$REL_VERSION_STRICT ;; mssql) echo "Using MS SQL Server..." docker run -d -p 1433:1433 --name db --network="host" -e "SA_PASSWORD=Password123!" -e "ACCEPT_EULA=Y" mcr.microsoft.com/mssql/server:2019-latest while ! docker exec db /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Password123!" -Q 'CREATE DATABASE wiki' &> /dev/null ; do echo "Waiting for database connection..." sleep 2 done docker run -d -p 3000:3000 --name wiki --network="host" -e "DB_TYPE=mssql" -e "DB_HOST=localhost" -e "DB_PORT=1433" -e "DB_NAME=wiki" -e "DB_USER=SA" -e "DB_PASS=Password123!" requarks/wiki:canary-$REL_VERSION_STRICT ;; sqlite) echo "Using SQLite..." docker run -d -p 3000:3000 --name wiki --network="host" -e "DB_TYPE=sqlite" -e "DB_FILEPATH=db.sqlite" requarks/wiki:canary-$REL_VERSION_STRICT ;; *) echo "Invalid DB Type!" ;; esac ================================================ FILE: dev/cypress/integration/setup.spec.js ================================================ /// describe('Setup', () => { it('Load the setup page', () => { cy.visit('/') cy.contains('You are about to install Wiki.js').should('exist') }) it('Enter administrator email address', () => { cy.get('.v-input').contains('Administrator Email').next('input').click().type('test@example.com') }) it('Enter a password', () => { cy.get('.v-input').contains('Password').next('input').click().type('12345678') cy.get('.v-input').contains('Confirm Password').next('input').click().type('12345678') }) it('Enter a Site URL', () => { cy.get('.v-input').contains('Site URL').next('input').click().clear().type('http://localhost:3000') }) it('Disable Telemetry', () => { cy.contains('Telemetry').next('.v-input').click() }) it('Press Install', () => { cy.get('.v-card__actions').find('button').click() }) it('Wait for install success', () => { cy.contains('Installation complete!', {timeout: 30000}).should('exist') }) // -> Disabled because of origin change errors during CI tests // // it('Redirect to login page', () => { // cy.location('pathname', {timeout: 10000}).should('include', '/login') // }) }) ================================================ FILE: dev/cypress/plugins/index.js ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config } ================================================ FILE: dev/cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) ================================================ FILE: dev/cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: dev/examples/docker-compose.yml ================================================ version: "3" services: db: image: postgres:15-alpine environment: POSTGRES_DB: wiki POSTGRES_PASSWORD: wikijsrocks POSTGRES_USER: wikijs logging: driver: "none" restart: unless-stopped volumes: - db-data:/var/lib/postgresql/data wiki: image: requarks/wiki:2 depends_on: - db environment: DB_TYPE: postgres DB_HOST: db DB_PORT: 5432 DB_USER: wikijs DB_PASS: wikijsrocks DB_NAME: wiki restart: unless-stopped ports: - "80:3000" - "443:3443" volumes: db-data: ================================================ FILE: dev/helm/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: dev/helm/Chart.yaml ================================================ apiVersion: v2 name: wiki version: '3.0.0' appVersion: '2' description: The most powerful and extensible open source Wiki software. keywords: - wiki - documentation - knowledge base - docs - reference - editor type: application home: https://js.wiki icon: https://cdn.js.wiki/images/wikijs-butterfly.svg sources: - https://github.com/requarks/wiki ================================================ FILE: dev/helm/README.md ================================================
Wiki.js [![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases) [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE) [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat&logo=javascript&logoColor=white)](http://standardjs.com/) [![Downloads](https://img.shields.io/github/downloads/Requarks/wiki/total.svg?style=flat&logo=github)](https://github.com/Requarks/wiki/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/requarks/wiki.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/requarks/wiki/) [![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml) [![Huntr](https://img.shields.io/badge/security%20bounty-disclose-brightgreen.svg?style=flat&logo=cachet&logoColor=white)](https://huntr.dev/bounties/disclose) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs) [![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack) [![Twitter Follow](https://img.shields.io/badge/follow-%40requarks-blue.svg?style=flat&logo=twitter)](https://twitter.com/requarks) [![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/) [![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe) ##### A modern, lightweight and powerful wiki app built on NodeJS
- **[Official Website](https://wiki.js.org/)** - **[Documentation](https://docs.requarks.io/)**

Donate

Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://wiki.js.org/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`). [![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship) [![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks) [![Donate on OpenCollective](https://img.shields.io/badge/donate-open%20collective-blue.svg?style=popout&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNTZweCIgaGVpZ2h0PSIyNTZweCIgdmlld0JveD0iMCAwIDI1NiAyNTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxnPjxwYXRoIGQ9Ik0yMDkuNzY1MTQ0LDEyOC4xNDk5NzkgQzIwOS43NjUxNDQsMTQ0LjE2MzMgMjA0Ljg2NDM4MSwxNTkuNDg5ODkgMTk2LjQ5ODc0NywxNzIuNzI1MDcyIEwyMjkuOTQ1Njc1LDIwNi4xNzE5OTkgQzI0Ni42ODIxMDUsMTgzLjg1Njc1OSAyNTUuNzI5MzA3LDE1Ni43MTUxNTIgMjU1LjcyOTMwNywxMjguODIxMTAyIEMyNTUuNzI5MzA3LDk5LjU1Njk5MTcgMjQ1Ljk3NDYwMyw3My4wNzEwMjA3IDIyOS4yNTg5NDQsNTEuNDg1ODEyOCBMMTk2LjQ4MzE0LDg0LjIxNDc5NCBDMjA1LjEyMjU2MSw5Ny4yMjI0NjgzIDIwOS43MzY5MDcsMTEyLjQ4NzgxIDIwOS43NDk1MzcsMTI4LjEwMzE1NiBMMjA5Ljc2NTE0NCwxMjguMTQ5OTc5IFoiIGZpbGw9IiNCOEQzRjQiPjwvcGF0aD48cGF0aCBkPSJNMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEM4Mi4xNDYwODcyLDIxMC4yNjg5NTggNDUuMzg3NTA5NCwxNzMuNTE3MzU4IDQ1LjI5MzAzOTMsMTI4LjE0OTk3OSBDNDUuMzYxNzUwMiw4Mi43NjQzMTM4IDgyLjEyNzg0ODcsNDUuOTg0MjU3IDEyNy41MTM0ODQsNDUuODk4MzE4NiBDMTQ0LjI0NDc1Miw0NS44OTgzMTg2IDE1OS41NzEzNDIsNTAuNzk5MDgxNyAxNzIuMTE5NzkyLDU5LjE2NDcxNTQgTDIwNC44NjQzODEsMjYuMzg4OTExNiBDMTgyLjU0MzY1LDkuNjY2NjUxMjkgMTU1LjQwMzQyOSwwLjYzMDg2MzI5OCAxMjcuNTEzNDg0LDAuNjM2NDk0NDAzIEM1Ny4xMjM1NDM3LDAuNjM2NDk0NDAzIDAsNTcuNzYwMDM4MSAwLDEyOC4xNDk5NzkgQzAsMTk4LjUwODcwNCA1Ny4xMjM1NDM3LDI1NS42NjM0NjMgMTI3LjUxMzQ4NCwyNTUuNjYzNDYzIEMxNTUuNTM3MzUyLDI1NS43NDA4NzYgMTgyLjc3NTk4OSwyNDYuNDA4NTEgMjA0Ljg2NDM4MSwyMjkuMTYxODg0IEwxNzEuNDE3NDU0LDE5NS43MzA1NjQgQzE1OS41NTU3MzQsMjA1LjQ4NTI2OCAxNDQuMjYwMzU5LDIxMC4zNTQ4MTYgMTI3LjUxMzQ4NCwyMTAuMzU0ODE2IEwxMjcuNTEzNDg0LDIxMC4zNTQ4MTYgWiIgZmlsbD0iIzdGQURGMiI+PC9wYXRoPjwvZz48L3N2Zz4=)](https://opencollective.com/wikijs) [![Donate via Paypal](https://img.shields.io/badge/donate-paypal-blue.svg?style=popout&logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) [![Donate via Ethereum](https://img.shields.io/badge/donate-ethereum-999.svg?style=popout&logo=ethereum&logoColor=CCC)](https://etherscan.io/address/0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5) [![Donate via Bitcoin](https://img.shields.io/badge/donate-bitcoin-ff9900.svg?style=popout&logo=bitcoin&logoColor=CCC)](https://checkout.opennode.com/p/2553c612-f863-4407-82b3-1a7685268747) [![Buy a T-Shirt](https://img.shields.io/badge/buy-t--shirts-teal.svg?style=popout&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4Igp3aWR0aD0iMjQiIGhlaWdodD0iMjQiCnZpZXdCb3g9IjAgMCAxOTIgMTkyIgpzdHlsZT0iIGZpbGw6IzAwMDAwMDsiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIGZvbnQtZmFtaWx5PSJub25lIiBmb250LXdlaWdodD0ibm9uZSIgZm9udC1zaXplPSJub25lIiB0ZXh0LWFuY2hvcj0ibm9uZSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0wLDE5MnYtMTkyaDE5MnYxOTJ6IiBmaWxsPSJub25lIj48L3BhdGg+PGcgZmlsbD0iIzFhYmM5YyI+PGcgaWQ9InN1cmZhY2UxIj48cGF0aCBkPSJNOTYsMGMtMTUuMjE4NzUsMCAtMjQuNjg3NSwzLjY1NjI1IC0yNS41LDRsLTIyLjUsNy4yNWMtMTAuNDA2MjUsMy4xODc1IC0xOS4wOTM3NSw5LjQzNzUgLTI1LjUsMTguMjVsLTIyLjUsNDIuNWwyNy4yNSwxNi43NWwxMi43NSwtMjR2MTE5LjI1YzAsNC40MDYyNSAyNS4wNjI1LDggNTYsOGMzMC45Mzc1LDAgNTYsLTMuNTkzNzUgNTYsLTh2LTExOS4yNWwxMi43NSwyNGwyNy4yNSwtMTYuNzVsLTIyLjUsLTQyLjVjLTYuNDA2MjUsLTguODEyNSAtMTUuMTU2MjUsLTE1LjA2MjUgLTI0Ljc1LC0xOC4yNWwtMjIuMjUsLTcuMjVjLTAuMTg3NSwwIC0xLjAzMTI1LDEuMzEyNSAtMiwyLjc1bDEuMjUsLTIuNWMwLDAgLTkuODQzNzUsLTQuMjUgLTI1Ljc1LC00LjI1ek05Niw4YzExLjQwNjI1LDAgMTguNDM3NSwyLjI1IDIxLDMuMjVjLTQuNDY4NzUsNS43NSAtMTEuNDA2MjUsMTIuNzUgLTIxLDEyLjc1Yy05LjQwNjI1LDAgLTE2LjQwNjI1LC03LjA2MjUgLTIwLjc1LC0xMi43NWMyLjg3NSwtMS4wNjI1IDkuODc1LC0zLjI1IDIwLjc1LC0zLjI1eiI+PC9wYXRoPjwvZz48L2c+PC9nPjwvc3ZnPg==)](https://wikijs.threadless.com)
## Introduction This chart bootstraps a Wiki.js deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. It also optionally deploys PostgreSQL as the database using the official PostgreSQL image from Docker Hub, but you are free to bring your own database. ## Prerequisites - PV provisioner support in the underlying infrastructure (with persistence storage enabled) if you want data persistance ## Adding the Wiki.js Helm Repository ```console $ helm repo add requarks https://charts.js.wiki ``` ## Installing the Chart To install the chart with the release name `my-release` run the following: ### Using Helm 3/4: ```console $ helm install my-release requarks/wiki ``` ### Using Helm 2: ```console $ helm install --name my-release requarks/wiki ``` The command deploys Wiki.js on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. > **Tip**: List all releases using `helm list` ## Uninstalling the Chart To uninstall/delete the `my-release` deployment: ```console $ helm delete my-release ``` The command removes all the Kubernetes components associated with the chart and deletes the release. > **Warning**: Persistant Volume Claims for the database are not deleted automatically. They need to be manually deleted ```console $ kubectl delete pvc/data-wiki-postgresql-0 ``` ## Configuration The following table lists the configurable parameters of the Wiki.js chart and their default values. | Parameter | Description | Default | | ------------------------------- | ------------------------------- | ---------------------------------------------------------- | | `image.repository` | Wiki.js image | `requarks/wiki` | | `image.tag` | Wiki.js image tag | `2` | | `imagePullPolicy` | Image pull policy | `IfNotPresent` | | `replicacount` | Number of Wiki.js pods to run | `1` | | `revisionHistoryLimit` | Total number of revision history points | `10` | | `resources.limits` | Wiki.js service resource limits | `nil` | | `resources.requests` | Wiki.js service resource requests | `nil` | | `nodeSelector` | Node labels for the Wiki.js pod assignment | `{}` | | `affinity` | Affinity settings for the Wiki.js pod assignment | `{}` | | `schedulerName` | Name of an alternate scheduler for the Wiki.js pod | `nil` | | `tolerations` | Toleration labels for the Wiki.js pod assignment | `[]` | | `volumeMounts` | Volume mounts for the Wiki.js container | `[]` | | `volumes` | Volumes for the Wiki.js pod | `[]` | | `ingress.enabled` | Enable ingress controller resource | `false` | | `ingress.className` | Ingress class name | `""` | | `ingress.annotations` | Ingress annotations | `{}` | | `ingress.hosts` | List of ingress rules | `[{"host": "wiki.local", "paths": ["/"]}]` | | `ingress.tls` | Ingress TLS configuration | `[]` | | `sideload.enabled` | Enable sideloading of locale files from git | `false` | | `sideload.repoURL` | Git repository URL containing locale files | `https://github.com/Requarks/wiki-localization` | | `sideload.env` | Environment variables for the sideload container | `{}` | | `sideload.securityContext` | Security context for the sideload container | `nil` | | `sideload.resources.limits` | Resource limits for the sideload container | `nil` | | `sideload.resources.requests` | Resource requests for the sideload container | `nil` | | `nodeExtraCaCerts` | Trusted certificates path | `nil` | | `externalPostgresql.databaseURL` | External postgres connection string | `nil` | | `postgresql.enabled` | Deploy postgres server (see below) | `true` | | `postgresql.postgresqlDatabase` | Postgres database name | `wiki` | | `postgresql.postgresqlUser` | Postgres username | `postgres` | | `postgresql.postgresqlHost` | Postgres host | `nil` | | `postgresql.postgresqlPassword` | Postgres password | `nil` | | `postgresql.existingSecret` | Provide an existing `Secret` for postgres | `nil` | | `postgresql.existingSecretKey` | The postgres password key in the existing `Secret` | `postgresql-password` | | `postgresql.existingSecretUserKey` | The postgres username key in the existing `Secret` | `postgresql-username` | | `postgresql.postgresqlPort` | Postgres port | `5432` | | `postgresql.ssl` | Enable external postgres SSL connection | `false` | | `postgresql.ca` | Certificate of Authority content for postgres | `nil` | | `postgresql.persistence.enabled` | Enable postgres persistence using PVC | `true` | | `postgresql.persistence.existingClaim` | Provide an existing `PersistentVolumeClaim` for postgres | `nil` | | `postgresql.persistence.storageClass` | Postgres PVC Storage Class (example: `nfs`) | `nil` | | `postgresql.persistence.size` | Postgres PVC Storage Request | `8Gi` | | `postgresql.persistence.accessMode` | Postgres Persistent Volume Access Mode | `ReadWriteOnce` | | `postgresql.image.repository` | PostgreSQL image repository | `postgres` | | `postgresql.image.tag` | PostgreSQL image tag | `18` | | `postgresql.image.pullPolicy` | PostgreSQL image pull policy | `IfNotPresent` | | `postgresql.resources` | PostgreSQL resource requests/limits | `{}` | | `postgresql.nodeSelector` | PostgreSQL node selector labels | `{}` | | `postgresql.tolerations` | PostgreSQL toleration labels | `[]` | | `postgresql.affinity` | PostgreSQL affinity settings | `{}` | | `postgresql.service.type` | PostgreSQL service type | `ClusterIP` | | `postgresql.service.port` | PostgreSQL service port | `5432` | | `postgresql.service.annotations` | PostgreSQL service annotations | `{}` | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, ```console $ helm install --name my-release \ --set postgresql.persistence.enabled=false \ requarks/wiki ``` Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, ```console $ helm install --name my-release -f values.yaml requarks/wiki ``` > **Tip**: You can use the default [values.yaml](values.yaml) ## PostgreSQL By default, PostgreSQL is installed as part of the chart using the official PostgreSQL image from Docker Hub (version 18). ### Using an external PostgreSQL server To use an external PostgreSQL server, set `postgresql.enabled` to `false`, then use either: #### Connection String Set `externalPostgresql.databaseURL` to the full PostgreSQL connection string. #### Connection Parameters Set `externalPostgresql.host`, `externalPostgres.port`, `externalPostgres.database`, `externalPostgres.username`, `externalPostgres.existingSecret` *(secret name)* and `externalPostgres.existingSecretKey` *(key in the secret containing the password)* Ensure the secret specified in `externalPostgresql.existingSecret` already exists, with a password set at the path specified in `externalPostgres.existingSecretKey`. To use an SSL connection you can set `externalPostgresql.ssl` to `true` and if needed the path to a Certificate of Authority can be set using `externalPostgresql.ca` to `/path/to/ca`. Default `externalPostgresql.ssl` value is `false`. ### Using an existing PostgreSQL secret with built-in PostgreSQL When using the built-in PostgreSQL (default behavior with `postgresql.enabled: true`), you can still use an existing Kubernetes secret for the database credentials by setting: - `postgresql.existingSecret`: Name of the existing secret containing the credentials - `postgresql.existingSecretKey`: Key in the secret containing the password (defaults to `postgresql-password`) - `postgresql.existingSecretUserKey`: Key in the secret containing the username (defaults to `postgresql-username`) Example usage: ```bash # Create your existing secret kubectl create secret generic my-postgres-secret \ --from-literal=postgresql-username=postgres \ --from-literal=postgresql-password=yourpassword # Deploy with existing secret helm install my-release requarks/wiki \ --set postgresql.enabled=true \ --set postgresql.existingSecret=my-postgres-secret ``` ## Persistence Persistent Volume Claims are used to keep the data across deployments. This is known to work in GCE, AWS, and minikube. See the [Configuration](#configuration) section to configure the PVC or to disable persistence. ## Ingress This chart provides support for Ingress resource. If you have an available Ingress Controller such as Nginx or Traefik you maybe want to set `ingress.enabled` to true and add `ingress.hosts` for the URL. Then, you should be able to access the installation using that address. ## Extra Trusted Certificates To append extra CA Certificates: 1. Create a ConfigMap with CAs in PEM format, e.g.: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: ca namespace: your-wikijs-namespace data: certs.pem: |- -----BEGIN CERTIFICATE----- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -----END CERTIFICATE----- ``` 2. Mount your CAs from the ConfigMap to the Wiki.js pod and set `nodeExtraCaCerts` helm variable. Insert the following lines to your Wiki.js `values.yaml`, e.g.: ```yaml volumeMounts: - name: ca mountPath: /cas.pem subPath: certs.pem volumes: - name: ca configMap: name: ca nodeExtraCaCerts: "/cas.pem" ``` ================================================ FILE: dev/helm/templates/NOTES.txt ================================================ 1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "wiki.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "wiki.fullname" . }}' export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "wiki.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "wiki.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 {{- end }} {{- if .Values.postgresql.enabled }} 2. PostgreSQL database has been deployed as part of this release: - Database: {{ .Values.postgresql.postgresqlDatabase }} - User: {{ .Values.postgresql.postgresqlUser }} - Service: {{ include "wiki.postgresql.fullname" . }} - Version: {{ .Values.postgresql.image.tag }} - Persistence: {{ .Values.postgresql.persistence.enabled | ternary "Enabled" "Disabled" }} {{- end }} {{- if not .Values.postgresql.enabled }} 2. External PostgreSQL setup detected. Ensure your database is accessible at the configured host. {{- end }} ================================================ FILE: dev/helm/templates/_helpers.tpl ================================================ {{/* vim: set filetype=mustache: */}} {{/* Expand the name of the chart. */}} {{- define "wiki.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "wiki.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} {{- $name := default .Chart.Name .Values.nameOverride -}} {{- if contains $name .Release.Name -}} {{- .Release.Name | trunc 63 | trimSuffix "-" -}} {{- else -}} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- end -}} {{- end -}} {{/* Create chart name and version as used by the chart label. */}} {{- define "wiki.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* Common labels */}} {{- define "wiki.labels" -}} helm.sh/chart: {{ include "wiki.chart" . }} {{ include "wiki.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end -}} {{/* Selector labels */}} {{- define "wiki.selectorLabels" -}} app.kubernetes.io/name: {{ include "wiki.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{/* Create the name of the service account to use */}} {{- define "wiki.serviceAccountName" -}} {{- if .Values.serviceAccount.create -}} {{ default (include "wiki.fullname" .) .Values.serviceAccount.name }} {{- else -}} {{ default "default" .Values.serviceAccount.name }} {{- end -}} {{- end -}} {{/* PostgreSQL fullname */}} {{- define "wiki.postgresql.fullname" -}} {{- printf "%s-%s" (include "wiki.fullname" .) "postgresql" | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* PostgreSQL selector labels */}} {{- define "wiki.postgresql.selectorLabels" -}} app.kubernetes.io/name: {{ include "wiki.name" . }}-postgresql app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{/* Set postgres host */}} {{- define "wiki.postgresql.host" -}} {{- if .Values.postgresql.enabled -}} {{- include "wiki.postgresql.fullname" . -}} {{- else -}} {{- .Values.postgresql.postgresqlHost | default "localhost" | quote -}} {{- end -}} {{- end -}} {{/* Set postgres secret */}} {{- define "wiki.postgresql.secret" -}} {{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}} {{- .Values.postgresql.existingSecret -}} {{- else if .Values.postgresql.enabled -}} {{- include "wiki.postgresql.fullname" . -}} {{- else -}} {{- template "wiki.fullname" . -}} {{- end -}} {{- end -}} {{/* Set postgres secretUserKey */}} {{- define "wiki.postgresql.secretUserKey" -}} {{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}} {{- default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote -}} {{- else if .Values.postgresql.enabled -}} "postgresql-username" {{- else -}} {{- default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote -}} {{- end -}} {{- end -}} {{/* Set postgres secretKey */}} {{- define "wiki.postgresql.secretKey" -}} {{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}} {{- default "postgresql-password" .Values.postgresql.existingSecretKey | quote -}} {{- else if .Values.postgresql.enabled -}} "postgresql-password" {{- else -}} {{- default "postgresql-password" .Values.postgresql.existingSecretKey | quote -}} {{- end -}} {{- end -}} {{/* Set postgres secretDatabaseKey */}} {{- define "wiki.postgresql.secretDatabaseKey" -}} {{- if and .Values.postgresql.enabled .Values.postgresql.existingSecret -}} {{- default "postgresql-database" .Values.postgresql.existingSecretDatabaseKey | quote -}} {{- else if .Values.postgresql.enabled -}} "postgresql-database" {{- else -}} {{- default "postgresql-database" .Values.postgresql.existingSecretDatabaseKey | quote -}} {{- end -}} {{- end -}} ================================================ FILE: dev/helm/templates/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "wiki.fullname" . }} labels: {{- include "wiki.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} selector: matchLabels: {{- include "wiki.selectorLabels" . | nindent 6 }} template: metadata: labels: {{- include "wiki.selectorLabels" . | nindent 8 }} annotations: {{- toYaml .Values.podAnnotations | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "wiki.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} {{- if .Values.sideload.enabled }} initContainers: - name: {{ .Chart.Name }}-sideload securityContext: {{- toYaml .Values.sideload.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ default "2" .Values.image.tag }}" imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }} env: {{- toYaml .Values.sideload.env | nindent 12 }} command: [ "sh", "-c" ] args: [ "mkdir -p /wiki/data/sideload && git clone --depth=1 {{ .Values.sideload.repoURL }} /wiki/data/sideload/" ] resources: {{- toYaml .Values.sideload.resources | nindent 12 }} {{- end }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ default "2" .Values.image.tag }}" imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }} env: {{- if .Values.nodeExtraCaCerts }} - name: NODE_EXTRA_CA_CERTS value: {{ .Values.nodeExtraCaCerts }} {{- end }} - name: DB_TYPE value: postgres {{- if and .Values.externalPostgresql .Values.externalPostgresql.databaseURL }} - name: DATABASE_URL value: {{ .Values.externalPostgresql.databaseURL }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ default "1" .Values.externalPostgresql.NODE_TLS_REJECT_UNAUTHORIZED | quote }} {{- else if .Values.postgresql.enabled }} - name: DB_HOST value: {{ template "wiki.postgresql.host" . }} - name: DB_PORT value: "{{ default "5432" .Values.postgresql.postgresqlPort }}" - name: DB_NAME value: {{ default "wiki" .Values.postgresql.postgresqlDatabase | quote }} - name: DB_USER {{- if .Values.postgresql.existingSecret }} valueFrom: secretKeyRef: name: {{ .Values.postgresql.existingSecret }} key: {{ template "wiki.postgresql.secretUserKey" . }} {{- else }} value: {{ default "postgres" .Values.postgresql.postgresqlUser }} {{- end }} - name: DB_SSL value: "{{ default "false" .Values.postgresql.ssl }}" - name: DB_SSL_CA value: "{{ default "" .Values.postgresql.ca }}" - name: DB_PASS valueFrom: secretKeyRef: name: {{ template "wiki.postgresql.secret" . }} key: {{ template "wiki.postgresql.secretKey" . }} {{- else if .Values.externalPostgresql }} # External PostgreSQL configuration - name: DB_HOST value: {{ required "External PostgreSQL host is required when postgresql.enabled is false" .Values.externalPostgresql.host | quote }} - name: DB_PORT value: {{ required "External PostgreSQL port is required when postgresql.enabled is false" .Values.externalPostgresql.port | quote }} - name: DB_NAME value: {{ required "External PostgreSQL database name is required when postgresql.enabled is false" .Values.externalPostgresql.database | quote }} - name: DB_USER value: {{ required "External PostgreSQL user is required when postgresql.enabled is false" .Values.externalPostgresql.username | quote }} - name: DB_PASS valueFrom: secretKeyRef: name: {{ required "External PostgreSQL secret name is required when postgresql.enabled is false" .Values.externalPostgresql.existingSecret | quote }} key: {{ required "External PostgreSQL secret key is required when postgresql.enabled is false" .Values.externalPostgresql.existingSecretKey | quote }} - name: DB_SSL value: "{{ default "false" .Values.externalPostgresql.ssl }}" - name: DB_SSL_CA value: "{{ default "" .Values.externalPostgresql.ca }}" {{- end }} - name: HA_ACTIVE value: {{ .Values.replicaCount | int | le 2 | quote }} {{- with .Values.extraEnvVars }} {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} ports: - name: http containerPort: 3000 protocol: TCP livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} startupProbe: {{- toYaml .Values.startupProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: dev/helm/templates/ingress.yaml ================================================ {{- if .Values.ingress.enabled -}} {{- $fullName := include "wiki.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1 {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "wiki.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} pathType: {{ .pathType }} {{- end }} backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: name: {{ $fullName }} port: number: {{ $svcPort }} {{- else }} serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} ================================================ FILE: dev/helm/templates/postgresql-pvc.yaml ================================================ {{- if and .Values.postgresql.enabled .Values.postgresql.persistence.enabled -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ include "wiki.postgresql.fullname" . }} labels: {{- include "wiki.labels" . | nindent 4 }} spec: accessModes: - {{ .Values.postgresql.persistence.accessMode | quote }} resources: requests: storage: {{ .Values.postgresql.persistence.size | quote }} {{- if .Values.postgresql.persistence.storageClass }} {{- if (eq "-" .Values.postgresql.persistence.storageClass) }} storageClassName: "" {{- else }} storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }} {{- end }} {{- end }} {{- end }} ================================================ FILE: dev/helm/templates/postgresql-secret.yaml ================================================ {{- if and .Values.postgresql.enabled (not .Values.postgresql.existingSecret) -}} apiVersion: v1 kind: Secret metadata: name: {{ include "wiki.postgresql.fullname" . }} labels: {{- include "wiki.labels" . | nindent 4 }} type: Opaque data: postgresql-password: {{ .Values.postgresql.postgresqlPassword | b64enc | quote }} postgresql-username: {{ .Values.postgresql.postgresqlUser | b64enc | quote }} {{- end }} ================================================ FILE: dev/helm/templates/postgresql-service.yaml ================================================ {{- if .Values.postgresql.enabled -}} apiVersion: v1 kind: Service metadata: name: {{ include "wiki.postgresql.fullname" . }} labels: {{- include "wiki.labels" . | nindent 4 }} {{- with .Values.postgresql.service.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: type: {{ .Values.postgresql.service.type }} ports: - port: {{ .Values.postgresql.service.port }} targetPort: 5432 protocol: TCP name: postgresql selector: {{- include "wiki.postgresql.selectorLabels" . | nindent 4 }} {{- end }} ================================================ FILE: dev/helm/templates/postgresql-statefulset.yaml ================================================ {{- if .Values.postgresql.enabled -}} apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "wiki.postgresql.fullname" . }} labels: {{- include "wiki.labels" . | nindent 4 }} spec: serviceName: {{ include "wiki.postgresql.fullname" . }} replicas: 1 selector: matchLabels: {{- include "wiki.postgresql.selectorLabels" . | nindent 6 }} template: metadata: labels: {{- include "wiki.postgresql.selectorLabels" . | nindent 8 }} spec: {{- with .Values.postgresql.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.postgresql.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.postgresql.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: postgresql image: {{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }} imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }} ports: - containerPort: 5432 name: postgresql env: - name: POSTGRES_DB value: {{ .Values.postgresql.postgresqlDatabase | quote }} - name: POSTGRES_USER {{- if .Values.postgresql.existingSecret }} valueFrom: secretKeyRef: name: {{ .Values.postgresql.existingSecret }} key: {{ default "postgresql-username" .Values.postgresql.existingSecretUserKey | quote }} {{- else }} valueFrom: secretKeyRef: name: {{ include "wiki.postgresql.fullname" . }} key: postgresql-username {{- end }} - name: POSTGRES_PASSWORD {{- if .Values.postgresql.existingSecret }} valueFrom: secretKeyRef: name: {{ .Values.postgresql.existingSecret }} key: {{ default "postgresql-password" .Values.postgresql.existingSecretKey | quote }} {{- else }} valueFrom: secretKeyRef: name: {{ include "wiki.postgresql.fullname" . }} key: postgresql-password {{- end }} - name: PGDATA value: /var/lib/postgresql/data/pgdata livenessProbe: exec: command: - sh - -c - exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }} initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 readinessProbe: exec: command: - sh - -c - exec pg_isready -U {{ .Values.postgresql.postgresqlUser }} -d {{ .Values.postgresql.postgresqlDatabase }} initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 6 resources: {{- toYaml .Values.postgresql.resources | nindent 12 }} volumeMounts: - name: postgresql-data mountPath: /var/lib/postgresql/data subPath: postgresql volumes: - name: postgresql-data {{- if .Values.postgresql.persistence.enabled }} persistentVolumeClaim: claimName: {{ include "wiki.postgresql.fullname" . }} {{- else }} emptyDir: {} {{- end }} {{- end }} ================================================ FILE: dev/helm/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{include "wiki.fullname" .}} labels: {{- include "wiki.labels" . | nindent 4 }} {{- with .Values.service.annotations }} annotations: {{- range $key, $value := . }} {{ $key }}: {{ $value | quote }} {{- end }} {{- end }} spec: type: {{.Values.service.type}} {{- if eq .Values.service.type "LoadBalancer" }} loadBalancerIP: {{ default "" .Values.service.loadBalancerIP }} {{- end }} ports: - port: {{ default "80" .Values.service.port}} targetPort: http protocol: TCP name: http selector: {{- include "wiki.selectorLabels" . | nindent 4}} ================================================ FILE: dev/helm/templates/serviceaccount.yaml ================================================ {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "wiki.serviceAccountName" . }} labels: {{- include "wiki.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end -}} ================================================ FILE: dev/helm/templates/tests/test-connection.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: "{{ include "wiki.fullname" . }}-test-connection" labels: {{- include "wiki.labels" . | nindent 4 }} annotations: "helm.sh/hook": test-success spec: containers: - name: wget image: busybox command: ['wget'] args: ['{{ include "wiki.fullname" . }}:{{ .Values.service.port }}'] restartPolicy: Never ================================================ FILE: dev/helm/values.yaml ================================================ # Default values for wiki. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 revisionHistoryLimit: 2 image: repository: requarks/wiki imagePullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: livenessProbe: httpGet: path: /healthz port: http readinessProbe: httpGet: path: /healthz port: http startupProbe: initialDelaySeconds: 15 periodSeconds: 5 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 60 httpGet: path: /healthz port: http podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 service: type: ClusterIP port: 80 # Annotations applied for services such as externalDNS or # service type LoadBalancer # type: LoadBalancer # annotations: {} # loadBalancerIP: 172.16.0.1 ingress: enabled: true className: "" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: wiki.minikube.local paths: - path: "/" pathType: Prefix tls: [] # - secretName: chart-example-tls # hosts: # - chart-example.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi nodeSelector: {} tolerations: [] affinity: {} volumeMounts: [] volumes: [] # This will allow us to install locales even without internet access using a initContainer & Wiki.js "sideloading" sideload: enabled: false # Git-Repo containing all locales.json-files you need: repoURL: https://github.com/requarks/wiki-localization ## This can be helpfull if you have internet access over a http proxy: env: [] # - name: HTTPS_PROXY # value: http://my.proxy.com:3128 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi ## Append extra trusted certificates for node process from extra volume via NODE_EXTRA_CA_CERTS variable # nodeExtraCaCerts: "/path/to/certs.pem" ## Additional environment variables to set extraEnvVars: [] # extraEnvVars: # - name: CUSTOM_VAR # value: "custom_value" # - name: SECRET_VAR # valueFrom: # secretKeyRef: # name: my-secret # key: secret-key ## This will override the postgresql chart values # externalPostgresql: # # note: ?sslmode=require => ?ssl=true # databaseURL: postgresql://postgres:postgres@postgres:5432/wiki?ssl=true # # For self signed CAs, like DigitalOcean # NODE_TLS_REJECT_UNAUTHORIZED: "0" ## Configuration for the custom PostgreSQL 18 deployment ## postgresql: enabled: true ## ssl enforce SSL communication with PostgresSQL ## Default to false ## ssl: false ## ca Certificate of Authority ## Default to empty, point to location of CA ## # ca: "path to ca" ## postgresqlHost override postgres database host ## Default to the service name of the custom PostgreSQL deployment ## postgresqlHost: "{{ include \"wiki.postgresql.fullname\" . }}" ## postgresqlPort port for postgres ## Default to 5432 ## postgresqlPort: 5432 ## PostgreSQL User to create. ## postgresqlUser: postgres ## PostgreSQL Database to create. ## postgresqlDatabase: wiki ## PostgreSQL password (will be stored in a secret) ## postgresqlPassword: "postgres" ## Use existing secret for PostgreSQL credentials ## If set, the chart will not create a new secret and will use the existing one ## # existingSecret: "my-existing-postgres-secret" ## Key in the existing secret containing the password ## # existingSecretKey: "postgresql-password" ## Key in the existing secret containing the username (defaults to "postgresql-username") ## # existingSecretUserKey: "postgresql-username" ## Persistent Volume Storage configuration. ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes ## persistence: ## Enable PostgreSQL persistence using Persistent Volume Claims. ## enabled: true ## concourse data Persistent Volume Storage Class ## If defined, storageClassName: ## If set to "-", storageClassName: "", which disables dynamic provisioning ## If undefined (the default) or set to null, no storageClassName spec is ## set, choosing the default provisioner. (gp2 on AWS, standard on ## GKE, AWS & OpenStack) ## # storageClass: "-" ## Persistent Volume Access Mode. ## accessMode: ReadWriteOnce ## Persistent Volume Storage Size. ## size: 8Gi ## PostgreSQL Image Configuration image: repository: postgres tag: "18" pullPolicy: IfNotPresent ## PostgreSQL Resources Configuration resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi ## PostgreSQL Node Selector, Tolerations and Affinity nodeSelector: {} tolerations: [] affinity: {} ## PostgreSQL Service Configuration service: type: ClusterIP port: 5432 # Additional service annotations annotations: {} ================================================ FILE: dev/index.js ================================================ #!/usr/bin/env node // =========================================== // Wiki.js DEV UTILITY // Licensed under AGPLv3 // =========================================== const _ = require('lodash') const chalk = require('chalk') const init = { dev() { const webpack = require('webpack') const chokidar = require('chokidar') console.info(chalk.yellow.bold('--- ====================== ---')) console.info(chalk.yellow.bold('--- Wiki.js DEVELOPER MODE ---')) console.info(chalk.yellow.bold('--- ====================== ---')) global.DEV = true global.WP_CONFIG = require('./webpack/webpack.dev.js') global.WP = webpack(global.WP_CONFIG) global.WP_DEV = { devMiddleware: require('webpack-dev-middleware')(global.WP, { publicPath: global.WP_CONFIG.output.publicPath }), hotMiddleware: require('webpack-hot-middleware')(global.WP) } global.WP_DEV.devMiddleware.waitUntilValid(() => { console.info(chalk.yellow.bold('>>> Starting Wiki.js in DEVELOPER mode...')) require('../server') process.stdin.setEncoding('utf8') process.stdin.on('data', data => { if (_.trim(data) === 'rs') { console.warn(chalk.yellow.bold('--- >>>>>>>>>>>>>>>>>>>>>>>> ---')) console.warn(chalk.yellow.bold('--- Manual restart requested ---')) console.warn(chalk.yellow.bold('--- <<<<<<<<<<<<<<<<<<<<<<<< ---')) this.reload() } }) const devWatcher = chokidar.watch([ './server', '!./server/views/master.pug' ], { cwd: process.cwd(), ignoreInitial: true, atomic: 400 }) devWatcher.on('ready', () => { devWatcher.on('all', _.debounce(() => { console.warn(chalk.yellow.bold('--- >>>>>>>>>>>>>>>>>>>>>>>>>>>> ---')) console.warn(chalk.yellow.bold('--- Changes detected: Restarting ---')) console.warn(chalk.yellow.bold('--- <<<<<<<<<<<<<<<<<<<<<<<<<<<< ---')) this.reload() }, 500)) }) }) }, async reload() { console.warn(chalk.yellow('--- Gracefully stopping server...')) await global.WIKI.kernel.shutdown(true) console.warn(chalk.yellow('--- Purging node modules cache...')) global.WIKI = {} Object.keys(require.cache).forEach(id => { if (/[/\\]server[/\\]/.test(id)) { delete require.cache[id] } }) Object.keys(module.constructor._pathCache).forEach(cacheKey => { if (/[/\\]server[/\\]/.test(cacheKey)) { delete module.constructor._pathCache[cacheKey] } }) console.warn(chalk.yellow('--- Unregistering process listeners...')) process.removeAllListeners('unhandledRejection') process.removeAllListeners('uncaughtException') require('../server') } } init.dev() ================================================ FILE: dev/installer/main.go ================================================ package main import ( "fmt" "runtime" "github.com/bugsnag/bugsnag-go" "github.com/fatih/color" "gopkg.in/AlecAivazis/survey.v1" ) var qs = []*survey.Question{ { Name: "location", Prompt: &survey.Input{ Message: "Where do you want to install Wiki.js?", Default: "./wiki", }, Validate: survey.Required, }, { Name: "dbtype", Prompt: &survey.Select{ Message: "Select a DB Driver:", Options: []string{"MariabDB", "MS SQL Server", "MySQL", "PostgreSQL", "SQLite"}, Default: "PostgreSQL", }, }, { Name: "port", Prompt: &survey.Input{ Message: "Server Port:", Default: "3000", }, }, } func main() { bugsnag.Configure(bugsnag.Configuration{ APIKey: "37770b3b08864599fd47c4edba5aa656", ReleaseStage: "dev", }) bold := color.New(color.FgWhite).Add(color.Bold) logo := ` __ __ _ _ _ _ / / /\ \ (_) | _(_) (_)___ \ \/ \/ / | |/ / | | / __| \ /\ /| | <| |_ | \__ \ \/ \/ |_|_|\_\_(_)/ |___/ |__/ ` color.Yellow(logo) bold.Println("\nInstaller for Wiki.js 2.x") fmt.Printf("%s-%s\n\n", runtime.GOOS, runtime.GOARCH) // Check system requirements bold.Println("Verifying system requirements...") CheckNodeJs() CheckRAM() fmt.Println() // the answers will be written to this struct answers := struct { Location string DBType string `survey:"dbtype"` Port int }{} // perform the questions err := survey.Ask(qs, &answers) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("%s chose %d.", answers.Location, answers.Port) // Download archives... bold.Println("\nDownloading packages...") // uiprogress.Start() // bar := uiprogress.AddBar(100) // bar.AppendCompleted() // bar.PrependElapsed() // for bar.Incr() { // time.Sleep(time.Millisecond * 20) // } finish := ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | | | Open http://localhost:3000/ in your browser | | to complete the installation! | | | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ` color.Yellow("\n\n" + finish) fmt.Println("Press any key to continue.") fmt.Scanln() } ================================================ FILE: dev/installer/syscheck.go ================================================ package main import ( "fmt" "log" "os/exec" "github.com/blang/semver" "github.com/fatih/color" "github.com/pbnjay/memory" ) const nodejsSemverRange = ">=8.11.4 <11.0.0" const ramMin = 768 // CheckNodeJs checks if Node.js is installed and has minimal supported version func CheckNodeJs() bool { cmd := exec.Command("node", "-v") cmdOutput, err := cmd.CombinedOutput() if err != nil { log.Fatal(err) } validRange := semver.MustParseRange(nodejsSemverRange) nodeVersion, err := semver.ParseTolerant(string(cmdOutput[:])) if !validRange(nodeVersion) { panic(fmt.Errorf(color.RedString("Error: Installed Node.js version %s is not supported! %s\n"), nodeVersion, nodejsSemverRange)) } fmt.Printf(color.GreenString("✔")+" Node.js %s: OK\n", nodeVersion.String()) return true } // CheckRAM checks if system total RAM meets requirements func CheckRAM() bool { var totalRAM = memory.TotalMemory() / 1024 / 1024 if totalRAM < ramMin { panic(fmt.Errorf(color.RedString("Error: System does not meet RAM requirements. %s MB minimum.\n"), ramMin)) } fmt.Printf(color.GreenString("✔")+" Total System RAM %d MB: OK\n", totalRAM) return true } // CheckNetworkAccess checks if download server can be reached func CheckNetworkAccess() bool { // TODO return true } ================================================ FILE: dev/openshift/Dockerfile ================================================ FROM requarks/wiki:2 USER root RUN chgrp -R 0 /wiki /logs && \ chmod -R g=u /wiki /logs USER 1001 ================================================ FILE: dev/packer/digitalocean.json ================================================ { "variables": { "do_api_token": "{{env `DIGITALOCEAN_API_TOKEN`}}", "image_name": "wikijs-snapshot-{{timestamp}}", "apt_packages": "software-properties-common", "application_name": "Wiki.js", "application_version": "{{env `WIKI_APP_VERSION`}}" }, "sensitive-variables": [ "do_api_token" ], "builders": [ { "type": "digitalocean", "api_token": "{{user `do_api_token`}}", "image": "ubuntu-24-04-x64", "region": "tor1", "size": "s-1vcpu-1gb", "ssh_username": "root", "snapshot_name": "{{user `image_name`}}" } ], "provisioners": [ { "type": "shell", "inline": [ "cloud-init status --wait" ] }, { "type": "file", "source": "scripts/001-onboot.sh", "destination": "/var/lib/cloud/scripts/per-instance/001-onboot.sh" }, { "type": "file", "source": "scripts/099-one-click", "destination": "/etc/update-motd.d/099-one-click" }, { "type": "shell", "inline": [ "chmod +x /var/lib/cloud/scripts/per-instance/001-onboot.sh", "chmod +x /etc/update-motd.d/099-one-click" ] }, { "type": "shell", "environment_vars": [ "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "LANG=en_US.UTF-8", "LC_CTYPE=en_US.UTF-8" ], "inline": [ "apt -qqy update", "apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade", "apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install {{user `apt_packages`}}", "apt-get -qqy clean" ] }, { "type": "shell", "environment_vars": [ "application_name={{user `application_name`}}", "application_version={{user `application_version`}}", "docker_compose_version={{user `docker_compose_version`}}", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "LANG=en_US.UTF-8", "LC_CTYPE=en_US.UTF-8" ], "scripts": [ "scripts/010-docker.sh", "scripts/011-ufw-docker.sh", "scripts/020-force-ssh-logout.sh", "scripts/900-cleanup.sh", "scripts/999-img-check.sh" ] } ] } ================================================ FILE: dev/packer/scripts/001-onboot.sh ================================================ #!/bin/bash # Generate PostgreSQL password openssl rand -base64 32 > /etc/wiki/.db-secret # Start containers if [[ -z $DATABASE_URL ]]; then docker start db fi docker start wiki docker start wiki-update-companion # Remove the ssh force logout command sed -e '/Match User root/d' \ -e '/.*ForceCommand.*droplet.*/d' \ -i /etc/ssh/sshd_config systemctl restart ssh ================================================ FILE: dev/packer/scripts/010-docker.sh ================================================ #!/bin/bash # Add Docker's official GPG key: sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: sudo tee /etc/apt/sources.list.d/docker.sources < /var/log/ufw.log ================================================ FILE: dev/packer/scripts/020-force-ssh-logout.sh ================================================ #!/bin/sh cat >> /etc/ssh/sshd_config < /root/.bash_history unset HISTFILE find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /root/.ssh/authorized_keys /etc/ssh/*key* touch /etc/ssh/revoked_keys chmod 600 /etc/ssh/revoked_keys # Securely erase the unused portion of the filesystem GREEN='\033[0;32m' NC='\033[0m' printf "\n${GREEN}Writing zeros to the remaining disk space to securely erase the unused portion of the file system. Depending on your disk size this may take several minutes. The secure erase will complete successfully when you see:${NC} dd: writing to '/zerofile': No space left on device\n Beginning secure erase now\n" dd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile ================================================ FILE: dev/packer/scripts/999-img-check.sh ================================================ #!/bin/bash # DigitalOcean Marketplace Image Validation Tool # © 2021-2022 DigitalOcean LLC. # This code is licensed under Apache 2.0 license (see LICENSE.md for details) VERSION="v. 1.8.1" RUNDATE=$( date ) # Script should be run with SUDO if [ "$EUID" -ne 0 ] then echo "[Error] - This script must be run with sudo or as the root user." exit 1 fi STATUS=0 PASS=0 WARN=0 FAIL=0 # $1 == command to check for # returns: 0 == true, 1 == false cmdExists() { if command -v "$1" > /dev/null 2>&1; then return 0 else return 1 fi } function getDistro { if [ -f /etc/os-release ]; then # freedesktop.org and systemd # shellcheck disable=SC1091 . /etc/os-release OS=$NAME VER=$VERSION_ID elif type lsb_release >/dev/null 2>&1; then # linuxbase.org OS=$(lsb_release -si) VER=$(lsb_release -sr) elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command # shellcheck disable=SC1091 . /etc/lsb-release OS=$DISTRIB_ID VER=$DISTRIB_RELEASE elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. OS=Debian VER=$(cat /etc/debian_version) elif [ -f /etc/SuSe-release ]; then # Older SuSE/etc. : elif [ -f /etc/redhat-release ]; then # Older Red Hat, CentOS, etc. VER=$(cut -d" " -f3 < /etc/redhat-release | cut -d "." -f1) d=$(cut -d" " -f1 < /etc/redhat-release | cut -d "." -f1) if [[ $d == "CentOS" ]]; then OS="CentOS Linux" fi else # Fall back to uname, e.g. "Linux ", also works for BSD, etc. OS=$(uname -s) VER=$(uname -r) fi } function loadPasswords { SHADOW=$(cat /etc/shadow) } function checkAgent { # Check for the presence of the DO directory in the filesystem if [ -d /opt/digitalocean ];then echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n" ((FAIL++)) STATUS=2 if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then echo "To uninstall the agent: 'sudo yum remove droplet-agent'" echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'" elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'" fi else echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n" ((PASS++)) fi } function checkLogs { cp_ignore="/var/log/cpanel-install.log" echo -en "\nChecking for log files in /var/log\n\n" # Check if there are log archives or log files that have not been recently cleared. for f in /var/log/*-????????; do [[ -e $f ]] || break if [ "${f}" != "${cp_ignore}" ]; then echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n" cat "${f}" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi done for f in /var/log/*.[0-9];do [[ -e $f ]] || break echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n" cat "${f}" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi done for f in /var/log/*.log; do [[ -e $f ]] || break if [[ "${f}" = '/var/log/lfd.log' && "$(grep -E -v '/var/log/messages has been reset| Watching /var/log/messages' "${f}" | wc -c)" -gt 50 ]]; then if [ "${f}" != "${cp_ignore}" ]; then echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n" cat "${f}" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi elif [[ "${f}" != '/var/log/lfd.log' && "$(wc -c < "${f}")" -gt 50 ]]; then if [ "${f}" != "${cp_ignore}" ]; then echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n" cat "${f}" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi fi done } function checkTMP { # Check the /tmp directory to ensure it is empty. Warn on any files found. return 1 } function checkRoot { user="root" uhome="/root" for usr in $SHADOW do IFS=':' read -r -a u <<< "$usr" if [[ "${u[0]}" == "${user}" ]]; then if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n" ((PASS++)) else echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n" ((FAIL++)) STATUS=2 fi fi done if [ -d ${uhome}/ ]; then if [ -d ${uhome}/.ssh/ ]; then if ls ${uhome}/.ssh/*> /dev/null 2>&1; then for key in "${uhome}"/.ssh/* do if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then if [ "$(wc -c < "${key}")" -gt 50 ]; then echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n" akey=$(cat "${key}") echo "File Contents:" echo "$akey" echo "--------------" ((FAIL++)) STATUS=2 fi elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then if [ "$(wc -c < "${key}")" -gt 0 ]; then echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n" akey=$(cat "${key}") echo "File Contents:" echo "$akey" echo "--------------" ((FAIL++)) STATUS=2 else echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi else if [ "$(wc -c < "${key}")" -gt 50 ]; then echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi fi done else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n" fi else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n" fi if [ -f /root/.bash_history ];then BH_S=$(wc -c < /root/.bash_history) if [[ $BH_S -lt 200 ]]; then echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n" ((PASS++)) else echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n" ((FAIL++)) STATUS=2 fi return 1; else echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n" ((PASS++)) fi else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n" fi echo -en "\n\n" return 1 } function checkUsers { # Check each user-created account awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' < /etc/passwd | while IFS= read -r user; do # Skip some other non-user system accounts if [[ $user == "centos" ]]; then : elif [[ $user == "nfsnobody" ]]; then : else echo -en "\nChecking user: ${user}...\n" for usr in $SHADOW do IFS=':' read -r -a u <<< "$usr" if [[ "${u[0]}" == "${user}" ]]; then if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n" # shellcheck disable=SC2030 ((PASS++)) else echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n" # shellcheck disable=SC2030 ((FAIL++)) STATUS=2 fi fi done #echo "User Found: ${user}" uhome="/home/${user}" if [ -d "${uhome}/" ]; then if [ -d "${uhome}/.ssh/" ]; then if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then for key in "${uhome}"/.ssh/* do if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then if [ "$(wc -c < "${key}")" -gt 50 ]; then echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n" akey=$(cat "${key}") echo "File Contents:" echo "$akey" echo "--------------" ((FAIL++)) STATUS=2 fi elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then if [ "$(wc -c < "${key}")" -gt 0 ]; then echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n" akey=$(cat "${key}") echo "File Contents:" echo "$akey" echo "--------------" ((FAIL++)) STATUS=2 else echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n" # shellcheck disable=SC2030 ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi else if [ "$(wc -c < "${key}")" -gt 50 ]; then echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n" ((WARN++)) if [[ $STATUS != 2 ]]; then STATUS=1 fi fi fi done else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n" fi else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n" fi else echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n" fi # Check for an uncleared .bash_history for this user if [ -f "${uhome}/.bash_history" ]; then BH_S=$(wc -c < "${uhome}/.bash_history") if [[ $BH_S -lt 200 ]]; then echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n" ((PASS++)) else echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n" ((FAIL++)) STATUS=2 fi echo -en "\n\n" fi fi done } function checkFirewall { if [[ $OS == "Ubuntu" ]]; then fw="ufw" ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //") if [[ $ufwa == "active" ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" # shellcheck disable=SC2031 ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" # shellcheck disable=SC2031 ((WARN++)) fi elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then if [ -f /usr/lib/systemd/system/csf.service ]; then fw="csf" if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) elif cmdExists "firewall-cmd"; then if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi else fw="firewalld" if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi fi elif [[ "$OS" =~ Debian.* ]]; then # user could be using a number of different services for managing their firewall # we will check some of the most common if cmdExists 'ufw'; then fw="ufw" ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //") if [[ $ufwa == "active" ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi elif cmdExists "firewall-cmd"; then fw="firewalld" if [[ $(systemctl is-active --quiet $fw) ]]; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi else # user could be using vanilla iptables, check if kernel module is loaded fw="iptables" if lsmod | grep -q '^ip_tables' 2>/dev/null; then FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" ((PASS++)) else FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" ((WARN++)) fi fi fi } function checkUpdates { if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then # Ensure /tmp exists and has the proper permissions before # checking for security updates # https://github.com/digitalocean/marketplace-partners/issues/94 if [[ ! -d /tmp ]]; then mkdir /tmp fi chmod 1777 /tmp echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n" apt-get -y update > /dev/null uc=$(apt-get --just-print upgrade | grep -i "security" -c) if [[ $uc -gt 0 ]]; then update_count=$(( uc / 2 )) else update_count=0 fi if [[ $update_count -gt 0 ]]; then echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n" echo -en echo -en "Here is a list of the security updates that are not installed:\n" sleep 2 apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++' echo -en # shellcheck disable=SC2031 ((FAIL++)) STATUS=2 else echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n" ((PASS++)) fi elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]] || [[ $OS == "CloudLinux" ]]; then echo -en "\nChecking for available security updates, this may take a minute...\n\n" update_count=$(yum check-update --security --quiet | wc -l) if [[ $update_count -gt 0 ]]; then echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n" ((FAIL++)) STATUS=2 else echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n" ((PASS++)) fi else echo "Error encountered" exit 1 fi return 1; } function checkCloudInit { if hash cloud-init 2>/dev/null; then CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n" ((PASS++)) else CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n" ((FAIL++)) STATUS=2 fi return 1 } function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } clear echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}" echo "Executed on: ${RUNDATE}" echo "Checking local system for Marketplace compatibility..." getDistro echo -en "\n\e[1mDistribution:\e[0m ${OS}\n" echo -en "\e[1mVersion:\e[0m ${VER}\n\n" ost=0 osv=0 if [[ $OS == "Ubuntu" ]]; then ost=1 if [[ $VER == "24.04" ]] || [[ $VER == "22.10" ]] || [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then osv=1 fi elif [[ "$OS" =~ Debian.* ]]; then ost=1 case "$VER" in 9) osv=1 ;; 10) osv=1 ;; 11) osv=1 ;; 12) osv=1 ;; 13) osv=1 ;; *) osv=2 ;; esac elif [[ $OS == "CentOS Linux" ]]; then ost=1 if [[ $VER == "8" ]]; then osv=1 elif [[ $VER == "7" ]]; then osv=1 elif [[ $VER == "6" ]]; then osv=1 else osv=2 fi elif [[ $OS == "CentOS Stream" ]]; then ost=1 if [[ $VER == "8" ]]; then osv=1 elif [[ $VER == "9" ]]; then osv=1 else osv=2 fi elif [[ $OS == "Rocky Linux" ]]; then ost=1 if [[ $VER =~ 8\. ]] || [[ $VER =~ 9\. ]]; then osv=1 else osv=2 fi elif [[ $OS == "AlmaLinux" ]]; then ost=1 if [[ "$VER" =~ 8.* ]] || [[ "$VER" =~ 9.* ]]; then osv=1 else osv=2 fi elif [[ $OS == "CloudLinux" ]]; then ost=1 if [[ "$VER" =~ 8.* ]] || [[ "$VER" =~ 9.* ]]; then osv=1 else osv=2 fi else ost=0 fi if [[ $ost == 1 ]]; then echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n" ((PASS++)) else echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n" ((FAIL++)) STATUS=2 fi if [[ $osv == 1 ]]; then echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n" ((PASS++)) elif [[ $ost == 1 ]]; then echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n" ((FAIL++)) STATUS=2 else echo "Exiting..." exit 1 fi checkCloudInit echo -en "${CI}" checkFirewall echo -en "${FW_VER}" checkUpdates loadPasswords checkLogs echo -en "\n\nChecking all user-created accounts...\n" checkUsers echo -en "\n\nChecking the root account...\n" checkRoot checkAgent # Source GPU compatibility check if [ -f "$(dirname "$0")/check_gpu_support.sh" ]; then source "$(dirname "$0")/check_gpu_support.sh" else echo "GPU check script not found. Skipping GPU compatibility checks." fi # Summary echo -en "\n\n---------------------------------------------------------------------------------------------------\n" if [[ $STATUS == 0 ]]; then echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n" elif [[ $STATUS == 1 ]]; then echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n" else echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n" fi echo "---------------------------------------------------------------------------------------------------" echo -en "\e[1m${PASS} Tests PASSED\e[0m\n" echo -en "\e[1m${WARN} WARNINGS\e[0m\n" echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n" echo -en "---------------------------------------------------------------------------------------------------\n" if [[ $STATUS == 0 ]]; then echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n" exit 0 elif [[ $STATUS == 1 ]]; then echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n" exit 0 else echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n" exit 1 fi ================================================ FILE: dev/search-engines/solr/solrconfig.xml ================================================ ================================================ FILE: dev/templates/legacy.pug ================================================ doctype html html head meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(charset='UTF-8') meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5') meta(name='theme-color', content='#1976d2') meta(name='msapplication-TileColor', content='#1976d2') meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png') title= pageMeta.title + ' | ' + config.title //- SEO / OpenGraph meta(name='description', content=pageMeta.description) meta(property='og:title', content=pageMeta.title) meta(property='og:type', content='website') meta(property='og:description', content=pageMeta.description) meta(property='og:image', content=pageMeta.image) meta(property='og:url', content=pageMeta.url) meta(property='og:site_name', content=config.title) //- Favicon link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png') link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-icon-192x192.png') link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png') link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png') link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2') link(rel='manifest', href='/_assets/manifest.json') //- Icon Set if config.theming.iconset === 'fa' link( type='text/css' rel='stylesheet' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css' ) else if config.theming.iconset === 'fa4' link( type='text/css' rel='stylesheet' href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css' ) //- CSS <% for (var index in htmlWebpackPlugin.files.css) { %> link( type='text/css' rel='stylesheet' href='<%= htmlWebpackPlugin.files.css[index] %>' ) <% } %> script( crossorigin='anonymous' src='https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=EventSource' ) //- JS <% for (var index in htmlWebpackPlugin.files.js) { %> script( type='text/javascript' src='<%= htmlWebpackPlugin.files.js[index] %>' ) <% } %> != analyticsCode.head block head body != analyticsCode.bodyStart block body != analyticsCode.bodyEnd ================================================ FILE: dev/templates/master.pug ================================================ doctype html html(lang=siteConfig.lang) head meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(charset='UTF-8') meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5') meta(name='theme-color', content='#1976d2') meta(name='msapplication-TileColor', content='#1976d2') meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png') title= pageMeta.title + ' | ' + config.title //- SEO / OpenGraph meta(name='description', content=pageMeta.description) meta(property='og:title', content=pageMeta.title) meta(property='og:type', content='website') meta(property='og:description', content=pageMeta.description) meta(property='og:image', content=pageMeta.image) meta(property='og:url', content=pageMeta.url) meta(property='og:site_name', content=config.title) //- Favicon link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png') link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png') link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png') link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png') link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2') link(rel='manifest', href='/_assets/manifest.json') //- Site Properties script. var siteConfig = !{JSON.stringify(siteConfig)} var siteLangs = !{JSON.stringify(langs)} //- Dev Mode Warning if devMode script. siteConfig.devMode = true //- Icon Set if config.theming.iconset === 'fa' link( type='text/css' rel='stylesheet' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css' ) else if config.theming.iconset === 'fa4' link( type='text/css' rel='stylesheet' href='https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css' ) //- CSS <% for (var index in htmlWebpackPlugin.files.css) { %> <% if (htmlWebpackPlugin.files.cssIntegrity) { %> link( type='text/css' rel='stylesheet' href='<%= htmlWebpackPlugin.files.css[index] %>' integrity=config.security.securitySRI ? '<%= htmlWebpackPlugin.files.cssIntegrity[index] %>' : false crossorigin='<%= webpackConfig.output.crossOriginLoading %>' ) <% } else { %> link( type='text/css' rel='stylesheet' href='<%= htmlWebpackPlugin.files.css[index] %>' ) <% } %> <% } %> //- JS <% for (var index in htmlWebpackPlugin.files.js) { %> <% if (htmlWebpackPlugin.files.jsIntegrity) { %> script( type='text/javascript' src='<%= htmlWebpackPlugin.files.js[index] %>' integrity=config.security.securitySRI ? '<%= htmlWebpackPlugin.files.jsIntegrity[index] %>' : false crossorigin='<%= webpackConfig.output.crossOriginLoading %>' ) <% } else { %> script( type='text/javascript' src='<%= htmlWebpackPlugin.files.js[index] %>' ) <% } %> <% } %> != analyticsCode.head block head body != analyticsCode.bodyStart block body != analyticsCode.bodyEnd ================================================ FILE: dev/templates/setup.pug ================================================ doctype html html head meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(charset='UTF-8') meta(name='viewport', content='user-scalable=yes, width=device-width, initial-scale=1, maximum-scale=5') meta(name='theme-color', content='#1976d2') meta(name='msapplication-TileColor', content='#1976d2') meta(name='msapplication-TileImage', content='/_assets/favicons/mstile-150x150.png') title Wiki.js Setup //- Favicon link(rel='apple-touch-icon', sizes='180x180', href='/_assets/favicons/apple-touch-icon.png') link(rel='icon', type='image/png', sizes='192x192', href='/_assets/favicons/android-chrome-192x192.png') link(rel='icon', type='image/png', sizes='32x32', href='/_assets/favicons/favicon-32x32.png') link(rel='icon', type='image/png', sizes='16x16', href='/_assets/favicons/favicon-16x16.png') link(rel='mask-icon', href='/_assets/favicons/safari-pinned-tab.svg', color='#1976d2') link(rel='manifest', href='/_assets/manifest.json') //- Site Lang script. var siteConfig = !{JSON.stringify({ title: config.title })} //- Dev Mode Warning if devMode script. siteConfig.devMode = true //- CSS <% for (var index in htmlWebpackPlugin.files.css) { %> link( type='text/css' rel='stylesheet' href='<%= htmlWebpackPlugin.files.css[index] %>' ) <% } %> //- JS <% for (var index in htmlWebpackPlugin.files.js) { %> script( type='text/javascript' src='<%= htmlWebpackPlugin.files.js[index] %>' ) <% } %> body #root setup(telemetry-id=telemetryClientID, wiki-version=packageObj.version) ================================================ FILE: dev/webpack/webpack.dev.js ================================================ const webpack = require('webpack') const path = require('path') const fs = require('fs-extra') const yargs = require('yargs').argv const _ = require('lodash') const { VueLoaderPlugin } = require('vue-loader') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin') const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin') const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin') const WriteFilePlugin = require('write-file-webpack-plugin') const WebpackBarPlugin = require('webpackbar') const babelConfig = fs.readJsonSync(path.join(process.cwd(), '.babelrc')) const cacheDir = '.webpack-cache/cache' const babelDir = path.join(process.cwd(), '.webpack-cache/babel') process.noDeprecation = true fs.emptyDirSync(path.join(process.cwd(), 'assets')) module.exports = { mode: 'development', entry: { app: ['./client/index-app.js', 'webpack-hot-middleware/client'], legacy: ['./client/index-legacy.js', 'webpack-hot-middleware/client'], setup: ['./client/index-setup.js', 'webpack-hot-middleware/client'] }, output: { path: path.join(process.cwd(), 'assets'), publicPath: '/_assets/', filename: 'js/[name].js', chunkFilename: 'js/[name].js', globalObject: 'this', pathinfo: true, crossOriginLoading: 'use-credentials' }, module: { rules: [ { test: /\.js$/, exclude: (modulePath) => { return modulePath.includes('node_modules') && !modulePath.includes('vuetify') }, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, { loader: 'babel-loader', options: { ...babelConfig, cacheDirectory: babelDir } } ] }, { test: /\.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] }, { test: /\.sass$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, 'style-loader', 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: false, sassOptions: { fiber: false } } } ] }, { test: /\.scss$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, 'style-loader', 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: false, sassOptions: { fiber: false } } }, { loader: 'sass-resources-loader', options: { resources: path.join(process.cwd(), '/client/scss/global.scss') } } ] }, { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.pug$/, exclude: [ path.join(process.cwd(), 'dev') ], loader: 'pug-plain-loader' }, { test: /\.(png|jpg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192 } } ] }, { test: /\.svg$/, exclude: [ path.join(process.cwd(), 'node_modules/grapesjs') ], use: [ { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'svg/' } } ] }, { test: /\.(graphql|gql)$/, exclude: /node_modules/, use: [ { loader: 'graphql-persisted-document-loader' }, { loader: 'graphql-tag/loader' } ] }, { test: /\.(woff2|woff|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { loader: 'webpack-modernizr-loader', test: /\.modernizrrc\.js$/ } ] }, plugins: [ new VueLoaderPlugin(), new VuetifyLoaderPlugin(), new MomentTimezoneDataPlugin({ startYear: 2017, endYear: (new Date().getFullYear()) + 5 }), new CopyWebpackPlugin({ patterns: [ { from: 'client/static' }, { from: './node_modules/prismjs/components', to: 'js/prism' } ] }), new HtmlWebpackPlugin({ template: 'dev/templates/master.pug', filename: '../server/views/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'legacy'] }), new HtmlWebpackPlugin({ template: 'dev/templates/legacy.pug', filename: '../server/views/legacy/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'app'] }), new HtmlWebpackPlugin({ template: 'dev/templates/setup.pug', filename: '../server/views/setup.pug', hash: false, inject: false, excludeChunks: ['app', 'legacy'] }), new HtmlWebpackPugPlugin(), new WebpackBarPlugin({ name: 'Client Assets' }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development'), 'process.env.CURRENT_THEME': JSON.stringify(_.defaultTo(yargs.theme, 'default')) }), new WriteFilePlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.WatchIgnorePlugin([ /node_modules/ ]) ], optimization: { namedModules: true, namedChunks: true, splitChunks: { cacheGroups: { default: { minChunks: 2, priority: -20, reuseExistingChunk: true }, vendor: { test: /[\\/]node_modules[\\/]/, minChunks: 2, priority: -10 } } }, runtimeChunk: 'single' }, resolve: { mainFields: ['browser', 'main', 'module'], symlinks: true, alias: { '@': path.join(process.cwd(), 'client'), 'vue$': 'vue/dist/vue.esm.js', 'gql': path.join(process.cwd(), 'client/graph'), // Duplicates fixes: 'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'), 'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'), 'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro'), 'modernizr$': path.resolve(process.cwd(), 'client/.modernizrrc.js') }, extensions: [ '.js', '.json', '.vue' ], modules: [ 'node_modules' ] }, node: { fs: 'empty' }, stats: { children: false, entrypoints: false }, target: 'web', watch: true } ================================================ FILE: dev/webpack/webpack.prod.js ================================================ const webpack = require('webpack') const path = require('path') const fs = require('fs-extra') const yargs = require('yargs').argv const _ = require('lodash') const { VueLoaderPlugin } = require('vue-loader') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin') const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin') const WebpackBarPlugin = require('webpackbar') const now = Math.round(Date.now() / 1000) const babelConfig = fs.readJsonSync(path.join(process.cwd(), '.babelrc')) const cacheDir = '.webpack-cache/cache' const babelDir = path.join(process.cwd(), '.webpack-cache/babel') process.noDeprecation = true fs.emptyDirSync(path.join(process.cwd(), 'assets')) module.exports = { mode: 'production', entry: { app: './client/index-app.js', legacy: './client/index-legacy.js', setup: './client/index-setup.js' }, output: { path: path.join(process.cwd(), 'assets'), publicPath: '/_assets/', filename: `js/[name].js?${now}`, chunkFilename: `js/[name].js?${now}`, globalObject: 'this', crossOriginLoading: 'use-credentials' }, module: { rules: [ { test: /\.js$/, exclude: (modulePath) => { return modulePath.includes('node_modules') && !modulePath.includes('vuetify') }, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, { loader: 'babel-loader', options: { ...babelConfig, cacheDirectory: babelDir } } ] }, { test: /\.css$/, use: [ 'style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader' ] }, { test: /\.sass$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, 'style-loader', 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: false, sassOptions: { fiber: false } } } ] }, { test: /\.scss$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: cacheDir } }, 'style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: false, sassOptions: { fiber: false } } }, { loader: 'sass-resources-loader', options: { resources: path.join(process.cwd(), '/client/scss/global.scss') } } ] }, { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.pug$/, exclude: [ path.join(process.cwd(), 'dev') ], loader: 'pug-plain-loader' }, { test: /\.(png|jpg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192 } } ] }, { test: /\.svg$/, exclude: [ path.join(process.cwd(), 'node_modules/grapesjs') ], use: [ { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'svg/' } } ] }, { test: /\.(graphql|gql)$/, exclude: /node_modules/, use: [ { loader: 'graphql-persisted-document-loader' }, { loader: 'graphql-tag/loader' } ] }, { test: /\.(woff2|woff|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { loader: 'webpack-modernizr-loader', test: /\.modernizrrc\.js$/ } ] }, plugins: [ new VueLoaderPlugin(), new VuetifyLoaderPlugin(), new webpack.BannerPlugin('Wiki.js - wiki.js.org - Licensed under AGPL'), new MomentTimezoneDataPlugin({ startYear: 2017, endYear: (new Date().getFullYear()) + 5 }), new CopyWebpackPlugin({ patterns: [ { from: 'client/static' }, { from: './node_modules/prismjs/components', to: 'js/prism' } ] }), new MiniCssExtractPlugin({ filename: 'css/bundle.[hash].css', chunkFilename: 'css/[name].[chunkhash].css' }), new HtmlWebpackPlugin({ template: 'dev/templates/master.pug', filename: '../server/views/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'legacy'] }), new HtmlWebpackPlugin({ template: 'dev/templates/legacy.pug', filename: '../server/views/legacy/master.pug', hash: false, inject: false, excludeChunks: ['setup', 'app'] }), new HtmlWebpackPlugin({ template: 'dev/templates/setup.pug', filename: '../server/views/setup.pug', hash: false, inject: false, excludeChunks: ['app', 'legacy'] }), new HtmlWebpackPugPlugin(), new ScriptExtHtmlWebpackPlugin({ sync: 'runtime.js', defaultAttribute: 'async' }), new WebpackBarPlugin({ name: 'Client Assets' }), new CleanWebpackPlugin(), new OptimizeCssAssetsPlugin({ cssProcessorOptions: { discardComments: { removeAll: true } }, canPrint: true }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.CURRENT_THEME': JSON.stringify(_.defaultTo(yargs.theme, 'default')) }), new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 50000 }) ], optimization: { namedModules: true, namedChunks: true, splitChunks: { name: 'vendor', minChunks: 2 }, runtimeChunk: 'single' }, resolve: { mainFields: ['browser', 'main', 'module'], symlinks: true, alias: { '@': path.join(process.cwd(), 'client'), 'vue$': 'vue/dist/vue.esm.js', 'gql': path.join(process.cwd(), 'client/graph'), // Duplicates fixes: 'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'), 'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'), 'uc.micro': path.join(process.cwd(), 'node_modules/uc.micro'), 'modernizr$': path.resolve(process.cwd(), 'client/.modernizrrc.js') }, extensions: [ '.js', '.json', '.vue' ], modules: [ 'node_modules' ] }, node: { fs: 'empty' }, stats: { children: false, entrypoints: false }, target: 'web' } ================================================ FILE: package.json ================================================ { "name": "wiki", "version": "2.0.0", "releaseDate": "2026-01-01T01:01:01.000Z", "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown", "main": "wiki.js", "dev": true, "scripts": { "start": "node server", "dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider node dev", "build": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js", "watch": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config dev/webpack/webpack.dev.js", "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest", "cypress:open": "cypress open", "postinstall": "patch-package" }, "repository": { "type": "git", "url": "git+https://github.com/Requarks/wiki.git" }, "keywords": [ "wiki", "wikis", "docs", "documentation", "markdown", "guides", "knowledge base" ], "author": "Nicolas Giard", "license": "AGPL-3.0", "bugs": { "url": "https://github.com/Requarks/wiki/issues" }, "homepage": "https://github.com/Requarks/wiki#readme", "engines": { "node": ">=20" }, "dependencies": { "@azure/storage-blob": "12.29.1", "@exlinc/keycloak-passport": "1.0.2", "@joplin/turndown-plugin-gfm": "1.0.45", "@root/csr": "0.8.1", "@root/keypairs": "0.10.3", "@root/pem": "1.0.4", "acme": "3.0.3", "akismet-api": "5.3.0", "algoliasearch": "4.5.1", "apollo-fetch": "0.7.0", "apollo-server": "2.25.2", "apollo-server-express": "2.25.2", "asciidoctor": "2.2.6", "auto-load": "3.0.4", "aws-sdk": "2.1693.0", "azure-search-client": "3.1.5", "bcryptjs-then": "1.0.1", "bluebird": "3.7.2", "body-parser": "1.20.1", "chalk": "4.1.0", "cheerio": "1.0.0-rc.5", "chokidar": "3.5.3", "chromium-pickle-js": "0.2.0", "clean-css": "5.3.3", "command-exists": "1.2.9", "compression": "1.8.1", "connect-session-knex": "2.0.0", "cookie-parser": "1.4.7", "cors": "2.8.5", "cuint": "0.2.2", "custom-error-instance": "2.1.2", "dependency-graph": "0.11.0", "diff": "4.0.2", "diff2html": "3.1.14", "dompurify": "3.3.1", "dotize": "0.3.0", "elasticsearch6": "npm:@elastic/elasticsearch@6", "elasticsearch7": "npm:@elastic/elasticsearch@7", "elasticsearch8": "npm:@elastic/elasticsearch@8", "emoji-regex": "10.2.1", "eventemitter2": "6.4.9", "express": "4.18.2", "express-brute": "1.0.1", "express-session": "1.18.2", "file-type": "15.0.1", "filesize": "6.1.0", "fs-extra": "9.0.1", "getos": "3.2.1", "graphql": "15.3.0", "graphql-list-fields": "2.0.2", "graphql-rate-limit-directive": "1.2.1", "graphql-subscriptions": "1.1.0", "graphql-tools": "7.0.0", "he": "1.2.0", "highlight.js": "10.3.1", "i18next": "19.8.3", "i18next-express-middleware": "2.0.0", "i18next-node-fs-backend": "2.1.3", "image-size": "0.9.2", "js-base64": "3.7.8", "js-binary": "1.2.0", "js-yaml": "3.14.0", "jsdom": "16.4.0", "jsonwebtoken": "9.0.3", "katex": "0.12.0", "klaw": "3.0.0", "knex": "0.21.7", "lodash": "4.17.21", "luxon": "1.25.0", "markdown-it": "11.0.1", "markdown-it-abbr": "1.0.4", "markdown-it-attrs": "3.0.3", "markdown-it-decorate": "1.2.2", "markdown-it-emoji": "3.0.0", "markdown-it-expand-tabs": "1.0.13", "markdown-it-external-links": "0.0.6", "markdown-it-footnote": "3.0.3", "markdown-it-imsize": "2.0.1", "markdown-it-mark": "3.0.1", "markdown-it-mathjax": "2.0.0", "markdown-it-multimd-table": "4.0.3", "markdown-it-pivot-table": "1.0.5", "markdown-it-sub": "1.0.0", "markdown-it-sup": "1.0.0", "markdown-it-task-lists": "2.1.1", "mathjax": "3.2.2", "mime-types": "2.1.35", "moment": "2.30.1", "moment-timezone": "0.6.0", "mongodb": "3.6.5", "ms": "2.1.3", "mssql": "6.2.3", "multer": "1.4.4", "mysql2": "3.16.0", "nanoid": "3.2.0", "node-2fa": "1.1.2", "node-cache": "5.1.2", "nodemailer": "6.9.1", "objection": "2.2.18", "passport": "0.4.1", "passport-auth0": "1.4.5", "passport-azure-ad": "4.3.5", "passport-cas": "0.1.1", "passport-discord": "0.1.4", "passport-dropbox-oauth2": "1.1.0", "passport-facebook": "3.0.0", "passport-github2": "0.1.12", "passport-gitlab2": "5.0.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", "passport-ldapauth": "3.0.1", "passport-local": "1.0.0", "passport-microsoft": "0.1.0", "passport-oauth2": "1.8.0", "passport-okta-oauth": "0.0.1", "passport-openidconnect": "0.1.2", "passport-saml": "3.2.4", "passport-slack-oauth2": "1.2.0", "passport-twitch-strategy": "2.2.0", "patch-package": "8.0.1", "pem-jwk": "2.0.0", "pg": "8.16.3", "pg-hstore": "2.3.4", "pg-pubsub": "0.8.1", "pg-query-stream": "4.10.3", "pg-tsquery": "8.4.2", "postinstall-postinstall": "2.1.0", "pug": "3.0.3", "punycode": "2.3.1", "qr-image": "3.2.0", "raven": "2.6.4", "remove-markdown": "0.6.2", "request": "2.88.2", "request-promise": "4.2.6", "safe-regex": "2.1.1", "sanitize-filename": "1.6.3", "scim-query-filter-parser": "2.0.4", "semver": "7.7.3", "serve-favicon": "2.5.1", "simple-git": "3.30.0", "solr-node": "1.2.1", "sqlite3": "5.1.7", "ssh2": "1.11.0", "ssh2-promise": "1.0.3", "striptags": "3.2.0", "subscriptions-transport-ws": "0.9.18", "tar-fs": "2.1.1", "turndown": "7.2.2", "twemoji": "14.0.2", "uslug": "1.0.4", "uuid": "9.0.0", "validate.js": "0.13.1", "winston": "3.8.2", "xss": "1.0.15", "yargs": "17.6.2" }, "devDependencies": { "@babel/cli": "^7.12.1", "@babel/core": "^7.12.3", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-decorators": "^7.12.1", "@babel/plugin-proposal-export-namespace-from": "^7.12.1", "@babel/plugin-proposal-function-sent": "^7.12.1", "@babel/plugin-proposal-json-strings": "^7.12.1", "@babel/plugin-proposal-numeric-separator": "^7.12.1", "@babel/plugin-proposal-throw-expressions": "^7.12.1", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.12.1", "@mdi/font": "5.8.55", "@panter/vue-i18next": "0.15.2", "@requarks/ckeditor5": "19.0.1-wiki.2", "@vue/babel-preset-app": "4.5.15", "animate-sass": "0.8.2", "animated-number-vue": "1.0.0", "apollo-cache-inmemory": "1.6.6", "apollo-client": "2.6.10", "apollo-link": "1.2.14", "apollo-link-batch-http": "1.2.14", "apollo-link-error": "1.1.13", "apollo-link-http": "1.5.17", "apollo-link-persisted-queries": "0.2.5", "apollo-link-ws": "1.0.20", "apollo-utilities": "1.3.4", "autoprefixer": "9.8.6", "babel-eslint": "10.1.0", "babel-jest": "26.6.1", "babel-loader": "^8.1.0", "babel-plugin-graphql-tag": "3.1.0", "babel-plugin-lodash": "3.3.4", "babel-plugin-prismjs": "2.0.1", "babel-plugin-transform-imports": "2.0.0", "cache-loader": "4.1.0", "canvas-confetti": "1.3.1", "cash-dom": "8.1.3", "chart.js": "2.9.4", "clean-webpack-plugin": "3.0.0", "clipboard": "2.0.11", "codemirror": "5.58.2", "codemirror-asciidoc": "1.0.4", "copy-webpack-plugin": "6.2.1", "core-js": "3.6.5", "cross-env": "10.0.0", "css-loader": "4.3.0", "cssnano": "4.1.10", "cypress": "5.3.0", "d3": "6.2.0", "duplicate-package-checker-webpack-plugin": "3.0.0", "epic-spinners": "1.1.0", "eslint": "7.12.0", "eslint-config-requarks": "1.0.7", "eslint-config-standard": "15.0.0", "eslint-plugin-cypress": "2.11.2", "eslint-plugin-import": "2.22.1", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "4.0.2", "eslint-plugin-vue": "7.1.0", "file-loader": "6.1.1", "filepond": "4.21.1", "filepond-plugin-file-validate-type": "1.2.8", "filesize.js": "2.0.0", "graphql-persisted-document-loader": "2.0.0", "graphql-tag": "2.11.0", "hammerjs": "2.0.8", "html-webpack-plugin": "4.5.0", "html-webpack-pug-plugin": "2.0.0", "i18next-chained-backend": "2.0.1", "i18next-localstorage-backend": "3.1.3", "i18next-xhr-backend": "3.2.2", "ignore-loader": "0.1.2", "jest": "26.6.1", "js-beautify": "1.13.5", "js-cookie": "2.2.1", "mermaid": "8.8.2", "mini-css-extract-plugin": "0.11.3", "moment-duration-format": "2.3.2", "moment-timezone-data-webpack-plugin": "1.3.0", "offline-plugin": "5.0.7", "optimize-css-assets-webpack-plugin": "5.0.4", "pako": "1.0.11", "postcss-cssnext": "3.1.1", "postcss-flexbugs-fixes": "4.2.1", "postcss-flexibility": "2.0.0", "postcss-import": "12.0.1", "postcss-loader": "3.0.0", "postcss-preset-env": "6.7.0", "postcss-selector-parser": "6.0.11", "prismjs": "1.22.0", "pug-lint": "2.6.0", "pug-loader": "2.4.0", "pug-plain-loader": "1.0.0", "raw-loader": "4.0.2", "resolve-url-loader": "3.1.2", "sass": "1.27.0", "sass-loader": "10.0.4", "sass-resources-loader": "2.1.1", "script-ext-html-webpack-plugin": "2.1.5", "simple-progress-webpack-plugin": "1.1.2", "style-loader": "1.3.0", "terser": "5.3.8", "twemoji-awesome": "1.0.6", "url-loader": "4.1.1", "velocity-animate": "1.5.2", "viz.js": "2.1.2", "vue": "2.6.14", "vue-apollo": "3.0.5", "vue-chartjs": "3.5.1", "vue-clipboards": "1.3.0", "vue-filepond": "6.0.3", "vue-hot-reload-api": "2.3.4", "vue-loader": "15.9.8", "vue-moment": "4.1.0", "vue-router": "3.4.7", "vue-status-indicator": "1.2.1", "vue-template-compiler": "2.6.14", "vue2-animate": "2.1.4", "vuedraggable": "2.24.3", "vuescroll": "4.16.1", "vuetify": "2.3.15", "vuetify-loader": "1.6.0", "vuex": "3.5.1", "vuex-pathify": "1.4.5", "vuex-persistedstate": "3.1.0", "webpack": "4.44.2", "webpack-bundle-analyzer": "3.9.0", "webpack-cli": "3.3.12", "webpack-dev-middleware": "3.7.2", "webpack-hot-middleware": "2.25.3", "webpack-merge": "5.2.0", "webpack-modernizr-loader": "5.0.0", "webpack-subresource-integrity": "1.5.1", "webpackbar": "4.0.0", "whatwg-fetch": "3.6.2", "write-file-webpack-plugin": "4.5.1", "xterm": "4.9.0", "zxcvbn": "4.4.2" }, "resolutions": { "apollo-server-express/**/graphql-tools": "4.0.8", "graphql": "15.3.0", "passport-saml/**/xml-crypto": "2.1.6" }, "browserslist": [ "> 1%", "last 2 major versions", "Firefox ESR", "not ie > 0", "not ie_mob > 0", "not android > 0", "not dead" ], "postcss": { "plugins": { "autoprefixer": {}, "cssnano": { "preset": [ "default", { "discardComments": { "removeAll": true } } ] }, "postcss-flexbugs-fixes": {}, "postcss-flexibility": {} } }, "pugLintConfig": { "disallowDuplicateAttributes": true, "disallowIdAttributeWithStaticValue": true, "disallowMultipleLineBreaks": true, "requireClassLiteralsBeforeAttributes": true, "requireIdLiteralsBeforeAttributes": true, "requireLineFeedAtFileEnd": true, "requireLowerCaseAttributes": true, "requireLowerCaseTags": true, "requireSpaceAfterCodeOperator": true, "requireStrictEqualityOperators": true, "validateAttributeQuoteMarks": "'", "validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": "\n " }, "validateDivTags": true, "validateIndentation": 2, "excludeFiles": [ "node_modules/**", "server/views/master.pug", "server/views/setup.pug", "server/views/legacy/master.pug" ] }, "collective": { "type": "opencollective", "url": "https://opencollective.com/wikijs", "logo": "https://opencollective.com/opencollective/logo.txt" } } ================================================ FILE: patches/extract-files+9.0.0.patch ================================================ diff --git a/node_modules/extract-files/package.json b/node_modules/extract-files/package.json index 636fa03..1b75f79 100644 --- a/node_modules/extract-files/package.json +++ b/node_modules/extract-files/package.json @@ -34,6 +34,9 @@ "import": "./public/index.mjs", "require": "./public/index.js" }, + "./public/extractFiles": "./public/extractFiles.js", + "./public/isExtractableFile": "./public/isExtractableFile.js", + "./public/ReactNativeFile": "./public/ReactNativeFile.js", "./public/": "./public/", "./package": "./package.json", "./package.json": "./package.json" ================================================ FILE: server/app/content/create.md ================================================ # Header ================================================ FILE: server/app/data.yml ================================================ # --------------------------------- # DO NOT EDIT THIS FILE! # This is reserved for system use! # --------------------------------- name: Wiki.js defaults: config: # File defaults port: 80 db: type: postgres host: localhost port: 5432 user: wikijs pass: wikijsrocks db: wiki ssl: false storage: ./db.sqlite sslOptions: auto: true ssl: enabled: false pool: min: 1 bindIP: 0.0.0.0 logLevel: info logFormat: default offline: false ha: false bodyParserLimit: 5mb # DB defaults api: isEnabled: false graphEndpoint: 'https://graph.requarks.io' lang: code: en autoUpdate: true namespaces: [] namespacing: false rtl: false telemetry: clientId: '' isEnabled: false title: Wiki.js company: '' contentLicense: '' footerOverride: '' logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg pageExtensions: - md - html - txt mail: host: '' secure: true verifySSL: true nav: mode: 'MIXED' theming: theme: 'default' iconset: 'md' darkMode: false tocPosition: 'left' auth: autoLogin: false enforce2FA: false hideLocal: false loginBgUrl: '' audience: 'urn:wiki.js' tokenExpiration: '30m' tokenRenewal: '14d' editShortcuts: editFab: true editMenuBar: false editMenuBtn: true editMenuExternalBtn: true editMenuExternalName: 'GitHub' editMenuExternalIcon: 'mdi-github' editMenuExternalUrl: 'https://github.com/org/repo/blob/main/{filename}' features: featurePageRatings: true featurePageComments: true featurePersonalWikis: true security: securityOpenRedirect: true securityIframe: true securityReferrerPolicy: true securityTrustProxy: false securitySRI: true securityHSTS: false securityHSTSDuration: 300 securityCSP: false securityCSPDirectives: '' server: sslRedir: false uploads: maxFileSize: 5242880 maxFiles: 10 scanSVG: true forceDownload: true flags: ldapdebug: false sqllog: false # System defaults channel: STABLE setup: false dataPath: ./data cors: credentials: true maxAge: 600 methods: 'GET,POST' origin: true search: maxHits: 100 maintainerEmail: security@requarks.io localeNamespaces: - admin - auth - common jobs: purgeUploads: onInit: true schedule: PT15M offlineSkip: false repeat: true syncGraphLocales: onInit: true schedule: P1D offlineSkip: true repeat: true syncGraphUpdates: onInit: true schedule: P1D offlineSkip: true repeat: true rebuildTree: onInit: true offlineSkip: false repeat: false immediate: true worker: true groups: defaultPermissions: - 'read:pages' - 'read:assets' - 'read:comments' - 'write:comments' defaultPageRules: - id: default deny: false match: START roles: - 'read:pages' - 'read:assets' - 'read:comments' - 'write:comments' path: '' locales: [] reservedPaths: - login - logout - register - verify - favicons - fonts - img - js - svg # --------------------------------- ================================================ FILE: server/app/regex.js ================================================ 'use strict' module.exports = { arabic: '\u0600-\u06ff\u0750-\u077f\ufb50-\ufc3f\ufe70-\ufefc', cjk: '\u4E00-\u9FBF\u3040-\u309F\u30A0-\u30FFㄱ-ㅎ가-힣ㅏ-ㅣ', youtube: /(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/, vimeo: /vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, dailymotion: /(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/ } ================================================ FILE: server/controllers/auth.js ================================================ /* global WIKI */ const express = require('express') const ExpressBrute = require('express-brute') const BruteKnex = require('../helpers/brute-knex') const router = express.Router() const _ = require('lodash') const commonHelper = require('../helpers/common') const bruteforce = new ExpressBrute(new BruteKnex({ createTable: true, knex: WIKI.models.knex }), { freeRetries: 5, minWait: 5 * 60 * 1000, // 5 minutes maxWait: 60 * 60 * 1000, // 1 hour failCallback: (req, res, next) => { res.status(401).send('Too many failed attempts. Try again later.') } }) /** * Login form */ router.get('/login', async (req, res, next) => { _.set(res.locals, 'pageMeta.title', 'Login') if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) { const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient() res.render('legacy/login', { err: false, formStrategies, socialStrategies }) } else { // -> Bypass Login if (WIKI.config.auth.autoLogin && !req.query.all) { const stg = await WIKI.models.authentication.query().orderBy('order').first() const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) if (!stgInfo.useForm) { return res.redirect(`/login/${stg.key}`) } } // -> Show Login const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg' res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal }) } }) /** * Social Strategies Login */ router.get('/login/:strategy', async (req, res, next) => { try { await WIKI.models.users.login({ strategy: req.params.strategy }, { req, res }) } catch (err) { next(err) } }) /** * Social Strategies Callback */ router.all('/login/:strategy/callback', async (req, res, next) => { if (req.method !== 'GET' && req.method !== 'POST') { return next() } try { const authResult = await WIKI.models.users.login({ strategy: req.params.strategy }, { req, res }) res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts()) const loginRedirect = req.cookies['loginRedirect'] const isValidRedirect = loginRedirect && loginRedirect.startsWith('/') && !loginRedirect.startsWith('//') && !loginRedirect.includes('://') if (loginRedirect === '/' && authResult.redirect) { res.clearCookie('loginRedirect') res.redirect(authResult.redirect) } else if (isValidRedirect) { res.clearCookie('loginRedirect') res.redirect(loginRedirect) } else { if (loginRedirect) { res.clearCookie('loginRedirect') } if (authResult.redirect) { res.redirect(authResult.redirect) } else { res.redirect('/') } } } catch (err) { next(err) } }) /** * LEGACY - Login form handling */ router.post('/login', bruteforce.prevent, async (req, res, next) => { _.set(res.locals, 'pageMeta.title', 'Login') if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) { try { const authResult = await WIKI.models.users.login({ strategy: req.body.strategy, username: req.body.user, password: req.body.pass }, { req, res }) req.brute.reset() res.cookie('jwt', authResult.jwt, commonHelper.getCookieOpts()) res.redirect('/') } catch (err) { const { formStrategies, socialStrategies } = await WIKI.models.authentication.getStrategiesForLegacyClient() res.render('legacy/login', { err, formStrategies, socialStrategies }) } } else { res.redirect('/login') } }) /** * Logout */ router.get('/logout', async (req, res) => { const redirURL = await WIKI.models.users.logout({ req, res }) req.logout() res.clearCookie('jwt') res.redirect(redirURL) }) /** * Register form */ router.get('/register', async (req, res, next) => { _.set(res.locals, 'pageMeta.title', 'Register') const localStrg = await WIKI.models.authentication.getStrategy('local') if (localStrg.selfRegistration) { res.render('register') } else { next(new WIKI.Error.AuthRegistrationDisabled()) } }) /** * Verify */ router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => { try { const usr = await WIKI.models.userKeys.validateToken({ kind: 'verify', token: req.params.token }) await WIKI.models.users.query().patch({ isVerified: true }).where('id', usr.id) req.brute.reset() if (WIKI.config.auth.enforce2FA) { res.redirect('/login') } else { const result = await WIKI.models.users.refreshToken(usr) res.cookie('jwt', result.token, commonHelper.getCookieOpts()) res.redirect('/') } } catch (err) { next(err) } }) /** * Reset Password */ router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => { try { const usr = await WIKI.models.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token }) if (!usr) { throw new Error('Invalid Token') } req.brute.reset() const changePwdContinuationToken = await WIKI.models.userKeys.generateToken({ userId: usr.id, kind: 'changePwd' }) const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg' res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken }) } catch (err) { next(err) } }) /** * JWT Public Endpoints */ router.get('/.well-known/jwk.json', function (req, res, next) { res.json(WIKI.config.certs.jwk) }) router.get('/.well-known/jwk.pem', function (req, res, next) { res.send(WIKI.config.certs.public) }) module.exports = router ================================================ FILE: server/controllers/common.js ================================================ const express = require('express') const router = express.Router() const pageHelper = require('../helpers/page') const _ = require('lodash') const CleanCSS = require('clean-css') const moment = require('moment') const qs = require('querystring') /* global WIKI */ const tmplCreateRegex = /^[0-9]+(,[0-9]+)?$/ /** * Robots.txt */ router.get('/robots.txt', (req, res, next) => { res.type('text/plain') if (_.includes(WIKI.config.seo.robots, 'noindex')) { res.send('User-agent: *\nDisallow: /') } else { res.status(200).end() } }) /** * Health Endpoint */ router.get('/healthz', (req, res, next) => { if (WIKI.models.knex.client.pool.numFree() < 1 && WIKI.models.knex.client.pool.numUsed() < 1) { res.status(503).json({ ok: false }).end() } else { res.status(200).json({ ok: true }).end() } }) /** * Administration */ router.get(['/a', '/a/*'], (req, res, next) => { if (!WIKI.auth.checkAccess(req.user, [ 'manage:system', 'write:users', 'manage:users', 'write:groups', 'manage:groups', 'manage:navigation', 'manage:theme', 'manage:api' ])) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'view' }) } _.set(res.locals, 'pageMeta.title', 'Admin') res.render('admin') }) /** * Download Page / Version */ router.get(['/d', '/d/*'], async (req, res, next) => { const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0 const page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, isPrivate: false }) pageArgs.tags = _.get(page, 'tags', []) if (versionId > 0) { if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'downloadVersion' }) } } else { if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'download' }) } } if (page) { const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType) res.attachment(fileName) if (versionId > 0) { const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId }) res.send(pageHelper.injectPageMetadata(pageVersion)) } else { res.send(pageHelper.injectPageMetadata(page)) } } else { res.status(404).end() } }) /** * Create/Edit document */ router.get(['/e', '/e/*'], async (req, res, next) => { const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { return res.redirect(`/e/${pageArgs.locale}/${pageArgs.path}`) } req.i18n.changeLanguage(pageArgs.locale) // -> Set Editor Lang _.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') // -> Check for reserved path if (pageHelper.isReservedPath(pageArgs.path)) { return next(new Error('Cannot create this page because it starts with a system reserved path.')) } // -> Get page data from DB let page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, isPrivate: false }) pageArgs.tags = _.get(page, 'tags', []) // -> Effective Permissions const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs) const injectCode = { css: WIKI.config.theming.injectCSS, head: WIKI.config.theming.injectHead, body: WIKI.config.theming.injectBody } if (page) { // -> EDIT MODE if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'edit' }) } // -> Get page tags await page.$relatedQuery('tags') page.tags = _.map(page.tags, 'tag') // Handle missing extra field page.extra = page.extra || { css: '', js: '' } // -> Beautify Script CSS if (!_.isEmpty(page.extra.css)) { page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles } _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`) _.set(res.locals, 'pageMeta.description', page.description) page.mode = 'update' page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false' page.content = Buffer.from(page.content).toString('base64') } else { // -> CREATE MODE if (!effectivePermissions.pages.write) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'create' }) } _.set(res.locals, 'pageMeta.title', `New Page`) page = { path: pageArgs.path, localeCode: pageArgs.locale, editorKey: null, mode: 'create', content: null, title: null, description: null, updatedAt: new Date().toISOString(), extra: { css: '', js: '' } } // -> From Template if (req.query.from && tmplCreateRegex.test(req.query.from)) { let tmplPageId = 0 let tmplVersionId = 0 if (req.query.from.indexOf(',')) { const q = req.query.from.split(',') tmplPageId = _.toSafeInteger(q[0]) tmplVersionId = _.toSafeInteger(q[1]) } else { tmplPageId = _.toSafeInteger(req.query.from) } if (tmplVersionId > 0) { // -> From Page Version const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId }) if (!pageVersion) { _.set(res.locals, 'pageMeta.title', 'Page Not Found') return res.status(404).render('notfound', { action: 'template' }) } if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'sourceVersion' }) } page.content = Buffer.from(pageVersion.content).toString('base64') page.editorKey = pageVersion.editor page.title = pageVersion.title page.description = pageVersion.description } else { // -> From Page Live const pageOriginal = await WIKI.models.pages.query().findById(tmplPageId) if (!pageOriginal) { _.set(res.locals, 'pageMeta.title', 'Page Not Found') return res.status(404).render('notfound', { action: 'template' }) } if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'source' }) } page.content = Buffer.from(pageOriginal.content).toString('base64') page.editorKey = pageOriginal.editorKey page.title = pageOriginal.title page.description = pageOriginal.description } } } res.render('editor', { page, injectCode, effectivePermissions }) }) /** * History */ router.get(['/h', '/h/*'], async (req, res, next) => { const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`) } req.i18n.changeLanguage(pageArgs.locale) _.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') const page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, isPrivate: false }) if (!page) { _.set(res.locals, 'pageMeta.title', 'Page Not Found') return res.status(404).render('notfound', { action: 'history' }) } pageArgs.tags = _.get(page, 'tags', []) const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs) if (!effectivePermissions.history.read) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.render('unauthorized', { action: 'history' }) } if (page) { _.set(res.locals, 'pageMeta.title', page.title) _.set(res.locals, 'pageMeta.description', page.description) res.render('history', { page, effectivePermissions }) } else { res.redirect(`/${pageArgs.path}`) } }) /** * Page ID redirection */ router.get(['/i', '/i/:id'], async (req, res, next) => { const pageId = _.toSafeInteger(req.params.id) if (pageId <= 0) { return res.redirect('/') } const page = await WIKI.models.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId) if (!page) { _.set(res.locals, 'pageMeta.title', 'Page Not Found') return res.status(404).render('notfound', { action: 'view' }) } if (!WIKI.auth.checkAccess(req.user, ['read:pages'], { locale: page.localeCode, path: page.path, private: page.isPrivate, privateNS: page.privateNS, explicitLocale: false, tags: page.tags })) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'view' }) } if (WIKI.config.lang.namespacing) { return res.redirect(`/${page.localeCode}/${page.path}`) } else { return res.redirect(`/${page.path}`) } }) /** * Profile */ router.get(['/p', '/p/*'], (req, res, next) => { if (!req.user || req.user.id < 1 || req.user.id === 2) { return res.status(403).render('unauthorized', { action: 'view' }) } _.set(res.locals, 'pageMeta.title', 'User Profile') res.render('profile') }) /** * Source */ router.get(['/s', '/s/*'], async (req, res, next) => { const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0 const page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, isPrivate: false }) pageArgs.tags = _.get(page, 'tags', []) if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`) } // -> Effective Permissions const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs) _.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') if (versionId > 0) { if (!effectivePermissions.history.read) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'sourceVersion' }) } } else { if (!effectivePermissions.source.read) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'source' }) } } if (page) { if (versionId > 0) { const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId }) _.set(res.locals, 'pageMeta.title', pageVersion.title) _.set(res.locals, 'pageMeta.description', pageVersion.description) res.render('source', { page: { ...page, ...pageVersion }, effectivePermissions }) } else { _.set(res.locals, 'pageMeta.title', page.title) _.set(res.locals, 'pageMeta.description', page.description) res.render('source', { page, effectivePermissions }) } } else { res.redirect(`/${pageArgs.path}`) } }) /** * Tags */ router.get(['/t', '/t/*'], (req, res, next) => { _.set(res.locals, 'pageMeta.title', 'Tags') res.render('tags') }) /** * User Avatar */ router.get('/_userav/:uid', async (req, res, next) => { if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) { return res.sendStatus(403) } const av = await WIKI.models.users.getUserAvatarData(req.params.uid) if (av) { res.set('Content-Type', 'image/jpeg') res.send(av) } return res.sendStatus(404) }) /** * View document / asset */ router.get('/*', async (req, res, next) => { const stripExt = _.some(WIKI.config.pageExtensions, ext => _.endsWith(req.path, `.${ext}`)) const pageArgs = pageHelper.parsePath(req.path, { stripExt }) const isPage = (stripExt || pageArgs.path.indexOf('.') === -1) if (isPage) { if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { const query = !_.isEmpty(req.query) ? `?${qs.stringify(req.query)}` : '' return res.redirect(`/${pageArgs.locale}/${pageArgs.path}${query}`) } req.i18n.changeLanguage(pageArgs.locale) try { // -> Get Page from cache const page = await WIKI.models.pages.getPage({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, isPrivate: false }) pageArgs.tags = _.get(page, 'tags', []) // -> Effective Permissions const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs) // -> Check User Access if (!effectivePermissions.pages.read) { if (req.user.id === 2) { res.cookie('loginRedirect', req.path, { maxAge: 15 * 60 * 1000 }) } if (pageArgs.path === 'home' && req.user.id === 2) { return res.redirect('/login') } _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'view' }) } _.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') if (page) { _.set(res.locals, 'pageMeta.title', page.title) _.set(res.locals, 'pageMeta.description', page.description) // -> Check Publishing State let pageIsPublished = page.isPublished if (pageIsPublished && !_.isEmpty(page.publishStartDate)) { pageIsPublished = moment(page.publishStartDate).isSameOrBefore() } if (pageIsPublished && !_.isEmpty(page.publishEndDate)) { pageIsPublished = moment(page.publishEndDate).isSameOrAfter() } if (!pageIsPublished && !effectivePermissions.pages.write) { _.set(res.locals, 'pageMeta.title', 'Unauthorized') return res.status(403).render('unauthorized', { action: 'view' }) } // -> Build sidebar navigation let sdi = 1 const sidebar = (await WIKI.models.navigation.getTree({ cache: true, locale: pageArgs.locale, groups: req.user.groups })).map(n => ({ i: `sdi-${sdi++}`, k: n.kind, l: n.label, c: n.icon, y: n.targetType, t: n.target })) // -> Build theme code injection const injectCode = { css: WIKI.config.theming.injectCSS, head: WIKI.config.theming.injectHead, body: WIKI.config.theming.injectBody } // Handle missing extra field page.extra = page.extra || { css: '', js: '' } if (!_.isEmpty(page.extra.css)) { injectCode.css = `${injectCode.css}\n${page.extra.css}` } if (!_.isEmpty(page.extra.js)) { injectCode.body = `${injectCode.body}\n${page.extra.js}` } if (req.query.legacy || (req.get('user-agent') && req.get('user-agent').indexOf('Trident') >= 0)) { // -> Convert page TOC if (_.isString(page.toc)) { page.toc = JSON.parse(page.toc) } // -> Render legacy view res.render('legacy/page', { page, sidebar, injectCode, isAuthenticated: req.user && req.user.id !== 2 }) } else { // -> Convert page TOC if (!_.isString(page.toc)) { page.toc = JSON.stringify(page.toc) } // -> Inject comments variables const commentTmpl = { codeTemplate: WIKI.data.commentProvider.codeTemplate, head: WIKI.data.commentProvider.head, body: WIKI.data.commentProvider.body, main: WIKI.data.commentProvider.main } if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) { [ { key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` }, { key: 'pageId', value: page.id } ].forEach((cfg) => { commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value) commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value) commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value) }) } // -> Page Filename (for edit on external repo button) let pageFilename = WIKI.config.lang.namespacing ? `${pageArgs.locale}/${page.path}` : page.path pageFilename += page.contentType === 'markdown' ? '.md' : '.html' // -> Render view res.render('page', { page, sidebar, injectCode, comments: commentTmpl, effectivePermissions, pageFilename }) } } else if (pageArgs.path === 'home') { _.set(res.locals, 'pageMeta.title', 'Welcome') res.render('welcome', { locale: pageArgs.locale }) } else { _.set(res.locals, 'pageMeta.title', 'Page Not Found') if (effectivePermissions.pages.write) { res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale }) } else { res.status(404).render('notfound', { action: 'view' }) } } } catch (err) { next(err) } } else { if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) { return res.sendStatus(403) } await WIKI.models.assets.getAsset(pageArgs.path, res) } }) module.exports = router ================================================ FILE: server/controllers/ssl.js ================================================ const express = require('express') const router = express.Router() const _ = require('lodash') const qs = require('querystring') /* global WIKI */ /** * Let's Encrypt Challenge */ router.get('/.well-known/acme-challenge/:token', (req, res, next) => { res.type('text/plain') if (_.get(WIKI.config, 'letsencrypt.challenge', false)) { if (WIKI.config.letsencrypt.challenge.token === req.params.token) { res.send(WIKI.config.letsencrypt.challenge.keyAuthorization) WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`) } else { res.status(406).send('Invalid Challenge Token!') WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`) } } else { res.status(418).end() } }) /** * Redirect to HTTPS if HTTP Redirection is enabled */ router.all('/*', (req, res, next) => { if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) { return res.redirect(`https://${req.hostname}${req.originalUrl}`) } else { next() } }) module.exports = router ================================================ FILE: server/controllers/upload.js ================================================ const express = require('express') const router = express.Router() const _ = require('lodash') const multer = require('multer') const path = require('path') const sanitize = require('sanitize-filename') /* global WIKI */ /** * Upload files */ router.post('/u', (req, res, next) => { multer({ dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'), limits: { fileSize: WIKI.config.uploads.maxFileSize, files: WIKI.config.uploads.maxFiles } }).array('mediaUpload')(req, res, next) }, async (req, res, next) => { if (!_.some(req.user.permissions, pm => _.includes(['write:assets', 'manage:system'], pm))) { return res.status(403).json({ succeeded: false, message: 'You are not authorized to upload files.' }) } else if (req.files.length < 1) { return res.status(400).json({ succeeded: false, message: 'Missing upload payload.' }) } else if (req.files.length > 1) { return res.status(400).json({ succeeded: false, message: 'You cannot upload multiple files within the same request.' }) } const fileMeta = _.get(req, 'files[0]', false) if (!fileMeta) { return res.status(500).json({ succeeded: false, message: 'Missing upload file metadata.' }) } // Get folder Id let folderId = null try { const folderRaw = _.get(req, 'body.mediaUpload', false) if (folderRaw) { folderId = _.get(JSON.parse(folderRaw), 'folderId', null) if (folderId === 0) { folderId = null } } else { throw new Error('Missing File Metadata') } } catch (err) { return res.status(400).json({ succeeded: false, message: 'Missing upload folder metadata.' }) } // Build folder hierarchy let hierarchy = [] if (folderId) { try { hierarchy = await WIKI.models.assetFolders.getHierarchy(folderId) } catch (err) { return res.status(400).json({ succeeded: false, message: 'Failed to fetch folder hierarchy.' }) } } // Sanitize filename fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\s,;#]+/g, '_')) // Check if user can upload at path const assetPath = (folderId) ? hierarchy.map(h => h.slug).join('/') + `/${fileMeta.originalname}` : fileMeta.originalname if (!WIKI.auth.checkAccess(req.user, ['write:assets'], { path: assetPath })) { return res.status(403).json({ succeeded: false, message: 'You are not authorized to upload files to this folder.' }) } // Process upload file await WIKI.models.assets.upload({ ...fileMeta, mode: 'upload', folderId: folderId, assetPath, user: req.user }) res.send('ok') }) router.get('/u', async (req, res, next) => { res.json({ ok: true }) }) module.exports = router ================================================ FILE: server/core/asar.js ================================================ const pickle = require('chromium-pickle-js') const path = require('path') const UINT64 = require('cuint').UINT64 const fs = require('fs') /* global WIKI */ /** * Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar) * by Fenglin Li (https://github.com/toyobayashi) */ const packages = { 'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`) } module.exports = { fdCache: {}, async serve (pkgName, req, res, next) { const file = this.readFilesystemSync(packages[pkgName]) const { filesystem, fd } = file const info = filesystem.getFile(req.path.substring(1)) if (info) { res.set({ 'Content-Type': 'image/svg+xml', 'Content-Length': info.size }) fs.createReadStream('', { fd, autoClose: false, start: 8 + filesystem.headerSize + parseInt(info.offset, 10), end: 8 + filesystem.headerSize + parseInt(info.offset, 10) + info.size - 1 }).on('error', (err) => { WIKI.logger.warn(err) res.sendStatus(404) }).pipe(res.status(200)) } else { res.sendStatus(404) } }, async unload () { const fds = Object.values(this.fdCache) if (fds.length > 0) { WIKI.logger.info('Closing ASAR file descriptors...') const closeAsync = require('util').promisify(fs.close) await Promise.all(fds.map(x => closeAsync(x.fd))) this.fdCache = {} } }, readArchiveHeaderSync (fd) { let size let headerBuf const sizeBuf = Buffer.alloc(8) if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) { throw new Error('Unable to read header size') } const sizePickle = pickle.createFromBuffer(sizeBuf) size = sizePickle.createIterator().readUInt32() headerBuf = Buffer.alloc(size) if (fs.readSync(fd, headerBuf, 0, size, null) !== size) { throw new Error('Unable to read header') } const headerPickle = pickle.createFromBuffer(headerBuf) const header = headerPickle.createIterator().readString() return { header: JSON.parse(header), headerSize: size } }, readFilesystemSync (archive) { if (!this.fdCache[archive]) { const fd = fs.openSync(archive, 'r') const header = this.readArchiveHeaderSync(fd) const filesystem = new Filesystem(archive) filesystem.header = header.header filesystem.headerSize = header.headerSize this.fdCache[archive] = { fd, filesystem: filesystem } } return this.fdCache[archive] } } class Filesystem { constructor (src) { this.src = path.resolve(src) this.header = { files: {} } this.offset = UINT64(0) } searchNodeFromDirectory (p) { let json = this.header const dirs = p.split(path.sep) for (const dir of dirs) { if (dir !== '.') { json = json.files[dir] } } return json } getNode (p) { const node = this.searchNodeFromDirectory(path.dirname(p)) const name = path.basename(p) if (name) { return node.files[name] } else { return node } } getFile (p, followLinks) { followLinks = typeof followLinks === 'undefined' ? true : followLinks const info = this.getNode(p) if (!info) { return false } if (info.link && followLinks) { return this.getFile(info.link) } else { return info } } } ================================================ FILE: server/core/auth.js ================================================ const passport = require('passport') const passportJWT = require('passport-jwt') const _ = require('lodash') const jwt = require('jsonwebtoken') const ms = require('ms') const { DateTime } = require('luxon') const crypto = require('crypto') const pem2jwk = require('pem-jwk').pem2jwk const randomBytesAsync = require('util').promisify(crypto.randomBytes) const commonHelper = require('../helpers/common') const securityHelper = require('../helpers/security') /* global WIKI */ module.exports = { strategies: {}, guest: { cacheExpiration: DateTime.utc().minus({ days: 1 }) }, groups: {}, validApiKeys: [], revocationList: require('./cache').init(), /** * Initialize the authentication module */ init() { this.passport = passport passport.serializeUser((user, done) => { done(null, user.id) }) passport.deserializeUser(async (id, done) => { try { const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => { builder.select('groups.id', 'permissions') }) if (user) { done(null, user) } else { done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null) } } catch (err) { done(err, null) } }) this.reloadGroups() this.reloadApiKeys() return this }, /** * Load authentication strategies */ async activateStrategies () { try { // Unload any active strategies WIKI.auth.strategies = {} const currentStrategies = _.keys(passport._strategies) _.pull(currentStrategies, 'session') _.forEach(currentStrategies, stg => { passport.unuse(stg) }) // Load JWT passport.use('jwt', new passportJWT.Strategy({ jwtFromRequest: securityHelper.extractJWT, secretOrKey: WIKI.config.certs.public, audience: WIKI.config.auth.audience, issuer: 'urn:wiki.js', algorithms: ['RS256'] }, (jwtPayload, cb) => { cb(null, jwtPayload) })) // Load enabled strategies const enabledStrategies = await WIKI.models.authentication.getStrategies() for (let idx in enabledStrategies) { const stg = enabledStrategies[idx] try { const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`) stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback` stg.config.key = stg.key strategy.init(passport, stg.config) strategy.config = stg.config WIKI.auth.strategies[stg.key] = { ...strategy, ...stg } WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`) } catch (err) { WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.key}): [ FAILED ]`) WIKI.logger.error(err) } } } catch (err) { WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`) WIKI.logger.error(err) } }, /** * Authenticate current request * * @param {Express Request} req * @param {Express Response} res * @param {Express Next Callback} next */ authenticate (req, res, next) { WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => { if (err) { return next() } let mustRevalidate = false // Expired but still valid within N days, just renew if (info instanceof Error && info.name === 'TokenExpiredError') { const expiredDate = (info.expiredAt instanceof Date) ? info.expiredAt.toISOString() : info.expiredAt if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) < DateTime.fromISO(expiredDate)) { mustRevalidate = true } } // Check if user / group is in revocation list if (user && !user.api && !mustRevalidate) { const uRevalidate = WIKI.auth.revocationList.get(`u${_.toString(user.id)}`) if (uRevalidate && user.iat < uRevalidate) { mustRevalidate = true } else if (DateTime.fromSeconds(user.iat) <= WIKI.startedAt) { // Prevent new / restarted instance from allowing revoked tokens mustRevalidate = true } else { for (const gid of user.groups) { const gRevalidate = WIKI.auth.revocationList.get(`g${_.toString(gid)}`) if (gRevalidate && user.iat < gRevalidate) { mustRevalidate = true break } } } } // Revalidate and renew token if (mustRevalidate) { const jwtPayload = jwt.decode(securityHelper.extractJWT(req)) try { const newToken = await WIKI.models.users.refreshToken(jwtPayload.id) user = newToken.user user.permissions = user.getGlobalPermissions() user.groups = user.getGroups() req.user = user // Try headers, otherwise cookies for response if (req.get('content-type') === 'application/json') { res.set('new-jwt', newToken.token) } else { res.cookie('jwt', newToken.token, commonHelper.getCookieOpts()) } // Avoid caching this response res.set('Cache-Control', 'no-store') } catch (errc) { WIKI.logger.warn(errc) return next() } } // JWT is NOT valid, set as guest if (!user) { if (WIKI.auth.guest.cacheExpiration <= DateTime.utc()) { WIKI.auth.guest = await WIKI.models.users.getGuestUser() WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 }) } req.user = WIKI.auth.guest return next() } // Process API tokens if (_.has(user, 'api')) { if (!WIKI.config.api.isEnabled) { return next(new Error('API is disabled. You must enable it from the Administration Area first.')) } else if (_.includes(WIKI.auth.validApiKeys, user.api)) { req.user = { id: 1, email: 'api@localhost', name: 'API', pictureUrl: null, timezone: 'America/New_York', localeCode: 'en', permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []), groups: [user.grp], getGlobalPermissions () { return req.user.permissions }, getGroups () { return req.user.groups } } return next() } else { return next(new Error('API Key is invalid or was revoked.')) } } // JWT is valid req.logIn(user, { session: false }, (errc) => { if (errc) { return next(errc) } next() }) })(req, res, next) }, /** * Check if user has access to resource * * @param {User} user * @param {Array} permissions * @param {String|Boolean} path */ checkAccess(user, permissions = [], page = false) { const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions() // System Admin if (_.includes(userPermissions, 'manage:system')) { return true } // Check Global Permissions if (_.intersection(userPermissions, permissions).length < 1) { return false } // Skip if no page rule to check if (!page) { return true } // Check Page Rules if (user.groups) { let checkState = { deny: false, match: false, specificity: '' } user.groups.forEach(grp => { const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => { if (rule.locales && rule.locales.length > 0) { if (!rule.locales.includes(page.locale)) { return } } if (_.intersection(rule.roles, permissions).length > 0) { switch (rule.match) { case 'START': if (_.startsWith(`/${page.path}`, `/${rule.path}`)) { checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] }) } break case 'END': if (_.endsWith(page.path, rule.path)) { checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] }) } break case 'REGEX': const reg = new RegExp(rule.path) if (reg.test(page.path)) { checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] }) } break case 'TAG': _.get(page, 'tags', []).forEach(tag => { if (tag.tag === rule.path) { checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT'] }) } }) break case 'EXACT': if (`/${page.path}` === `/${rule.path}`) { checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] }) } break } } }) }) return (checkState.match && !checkState.deny) } return false }, /** * Check for exclusive permissions (contain any X permission(s) but not any Y permission(s)) * * @param {User} user * @param {Array} includePermissions * @param {Array} excludePermissions */ checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) { const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions() // Check Inclusion Permissions if (_.intersection(userPermissions, includePermissions).length < 1) { return false } // Check Exclusion Permissions if (_.intersection(userPermissions, excludePermissions).length > 0) { return false } return true }, /** * Check and apply Page Rule specificity * * @access private */ _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) { if (rule.path.length === checkState.specificity.length) { // Do not override higher priority rules if (_.includes(higherPriority, checkState.match)) { return checkState } // Do not override a previous DENY rule with same match if (rule.match === checkState.match && checkState.deny && !rule.deny) { return checkState } } else if (rule.path.length < checkState.specificity.length) { // Do not override higher specificity rules return checkState } return { deny: rule.deny, match: rule.match, specificity: rule.path } }, /** * Reload Groups from DB */ async reloadGroups () { const groupsArray = await WIKI.models.groups.query() this.groups = _.keyBy(groupsArray, 'id') WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 }) }, /** * Reload valid API Keys from DB */ async reloadApiKeys () { const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', DateTime.utc().toISO()) this.validApiKeys = _.map(keys, 'id') }, /** * Generate New Authentication Public / Private Key Certificates */ async regenerateCertificates () { WIKI.logger.info('Regenerating certificates...') _.set(WIKI.config, 'sessionSecret', (await randomBytesAsync(32)).toString('hex')) const certs = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem', cipher: 'aes-256-cbc', passphrase: WIKI.config.sessionSecret } }) _.set(WIKI.config, 'certs', { jwk: pem2jwk(certs.publicKey), public: certs.publicKey, private: certs.privateKey }) await WIKI.configSvc.saveToDb([ 'certs', 'sessionSecret' ]) await WIKI.auth.activateStrategies() WIKI.events.outbound.emit('reloadAuthStrategies') WIKI.logger.info('Regenerated certificates: [ COMPLETED ]') }, /** * Reset Guest User */ async resetGuestUser() { WIKI.logger.info('Resetting guest account...') const guestGroup = await WIKI.models.groups.query().where('id', 2).first() await WIKI.models.users.query().delete().where({ providerKey: 'local', email: 'guest@example.com' }).orWhere('id', 2) const guestUser = await WIKI.models.users.query().insert({ id: 2, provider: 'local', email: 'guest@example.com', name: 'Guest', password: '', locale: 'en', defaultEditor: 'markdown', tfaIsActive: false, isSystem: true, isActive: true, isVerified: true }) await guestUser.$relatedQuery('groups').relate(guestGroup.id) WIKI.logger.info('Guest user has been reset: [ COMPLETED ]') }, /** * Subscribe to HA propagation events */ subscribeToEvents() { WIKI.events.inbound.on('reloadGroups', () => { WIKI.auth.reloadGroups() }) WIKI.events.inbound.on('reloadApiKeys', () => { WIKI.auth.reloadApiKeys() }) WIKI.events.inbound.on('reloadAuthStrategies', () => { WIKI.auth.activateStrategies() }) WIKI.events.inbound.on('addAuthRevoke', (args) => { WIKI.auth.revokeUserTokens(args) }) }, /** * Get all user permissions for a specific page */ getEffectivePermissions (req, page) { return { comments: { read: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['read:comments'], page) : false, write: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['write:comments'], page) : false, manage: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['manage:comments'], page) : false }, history: { read: WIKI.auth.checkAccess(req.user, ['read:history'], page) }, source: { read: WIKI.auth.checkAccess(req.user, ['read:source'], page) }, pages: { read: WIKI.auth.checkAccess(req.user, ['read:pages'], page), write: WIKI.auth.checkAccess(req.user, ['write:pages'], page), manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page), delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page), script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page), style: WIKI.auth.checkAccess(req.user, ['write:styles'], page) }, system: { manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page) } } }, /** * Add user / group ID to JWT revocation list, forcing all requests to be validated against the latest permissions */ revokeUserTokens ({ id, kind = 'u' }) { WIKI.auth.revocationList.set(`${kind}${_.toString(id)}`, Math.round(DateTime.utc().minus({ seconds: 5 }).toSeconds()), Math.ceil(ms(WIKI.config.auth.tokenExpiration) / 1000)) } } ================================================ FILE: server/core/cache.js ================================================ const NodeCache = require('node-cache') module.exports = { init() { return new NodeCache() } } ================================================ FILE: server/core/config.js ================================================ const _ = require('lodash') const chalk = require('chalk') const cfgHelper = require('../helpers/config') const fs = require('fs') const path = require('path') const yaml = require('js-yaml') /* global WIKI */ module.exports = { /** * Load root config from disk */ init() { let confPaths = { config: path.join(WIKI.ROOTPATH, 'config.yml'), data: path.join(WIKI.SERVERPATH, 'app/data.yml'), dataRegex: path.join(WIKI.SERVERPATH, 'app/regex.js') } if (process.env.dockerdev) { confPaths.config = path.join(WIKI.ROOTPATH, `dev/containers/config.yml`) } if (process.env.CONFIG_FILE) { confPaths.config = path.resolve(WIKI.ROOTPATH, process.env.CONFIG_FILE) } process.stdout.write(chalk.blue(`Loading configuration from ${confPaths.config}... `)) let appconfig = {} let appdata = {} try { appconfig = yaml.safeLoad( cfgHelper.parseConfigValue( fs.readFileSync(confPaths.config, 'utf8') ) ) appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8')) appdata.regex = require(confPaths.dataRegex) console.info(chalk.green.bold(`OK`)) } catch (err) { console.error(chalk.red.bold(`FAILED`)) console.error(err.message) console.error(chalk.red.bold(`>>> Unable to read configuration file! Did you create the config.yml file?`)) process.exit(1) } // Merge with defaults appconfig = _.defaultsDeep(appconfig, appdata.defaults.config) if (appconfig.port < 1 || process.env.HEROKU) { appconfig.port = process.env.PORT || 80 } const packageInfo = require(path.join(WIKI.ROOTPATH, 'package.json')) // Load DB Password from Docker Secret File if (process.env.DB_PASS_FILE) { console.info(chalk.blue(`DB_PASS_FILE is defined. Will use secret from file.`)) try { appconfig.db.pass = fs.readFileSync(process.env.DB_PASS_FILE, 'utf8').trim() } catch (err) { console.error(chalk.red.bold(`>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!`)) console.error(err.message) process.exit(1) } } WIKI.config = appconfig WIKI.data = appdata WIKI.version = packageInfo.version WIKI.releaseDate = packageInfo.releaseDate WIKI.devMode = (packageInfo.dev === true) }, /** * Load config from DB */ async loadFromDb() { let conf = await WIKI.models.settings.getConfig() if (conf) { WIKI.config = _.defaultsDeep(conf, WIKI.config) } else { WIKI.logger.warn('DB Configuration is empty or incomplete. Switching to Setup mode...') WIKI.config.setup = true } }, /** * Save config to DB * * @param {Array} keys Array of keys to save * @returns Promise */ async saveToDb(keys, propagate = true) { try { for (let key of keys) { let value = _.get(WIKI.config, key, null) if (!_.isPlainObject(value)) { value = { v: value } } let affectedRows = await WIKI.models.settings.query().patch({ value }).where('key', key) if (affectedRows === 0 && value) { await WIKI.models.settings.query().insert({ key, value }) } } if (propagate) { WIKI.events.outbound.emit('reloadConfig') } } catch (err) { WIKI.logger.error(`Failed to save configuration to DB: ${err.message}`) return false } return true }, /** * Apply Dev Flags */ async applyFlags() { WIKI.models.knex.client.config.debug = WIKI.config.flags.sqllog }, /** * Subscribe to HA propagation events */ subscribeToEvents() { WIKI.events.inbound.on('reloadConfig', async () => { await WIKI.configSvc.loadFromDb() await WIKI.configSvc.applyFlags() }) } } ================================================ FILE: server/core/db.js ================================================ const _ = require('lodash') const autoload = require('auto-load') const path = require('path') const Promise = require('bluebird') const Knex = require('knex') const fs = require('fs') const Objection = require('objection') const migrationSource = require('../db/migrator-source') const migrateFromBeta = require('../db/beta') /* global WIKI */ /** * ORM DB module */ module.exports = { Objection, knex: null, listener: null, /** * Initialize DB * * @return {Object} DB instance */ init() { let self = this // Fetch DB Config let dbClient = null let dbConfig = (!_.isEmpty(process.env.DATABASE_URL)) ? process.env.DATABASE_URL : { host: WIKI.config.db.host.toString(), user: WIKI.config.db.user.toString(), password: WIKI.config.db.pass.toString(), database: WIKI.config.db.db.toString(), port: WIKI.config.db.port } // Handle SSL Options let dbUseSSL = (WIKI.config.db.ssl === true || WIKI.config.db.ssl === 'true' || WIKI.config.db.ssl === 1 || WIKI.config.db.ssl === '1') let sslOptions = null if (dbUseSSL && _.isPlainObject(dbConfig) && _.get(WIKI.config.db, 'sslOptions.auto', null) === false) { sslOptions = WIKI.config.db.sslOptions sslOptions.rejectUnauthorized = sslOptions.rejectUnauthorized !== false if (sslOptions.ca && sslOptions.ca.indexOf('-----') !== 0) { sslOptions.ca = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.ca)) } if (sslOptions.cert) { sslOptions.cert = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.cert)) } if (sslOptions.key) { sslOptions.key = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.key)) } if (sslOptions.pfx) { sslOptions.pfx = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.pfx)) } } else { sslOptions = true } // Handle inline SSL CA Certificate mode if (!_.isEmpty(process.env.DB_SSL_CA)) { const chunks = [] for (let i = 0, charsLength = process.env.DB_SSL_CA.length; i < charsLength; i += 64) { chunks.push(process.env.DB_SSL_CA.substring(i, i + 64)) } dbUseSSL = true sslOptions = { rejectUnauthorized: true, ca: '-----BEGIN CERTIFICATE-----\n' + chunks.join('\n') + '\n-----END CERTIFICATE-----\n' } } // Engine-specific config switch (WIKI.config.db.type) { case 'postgres': dbClient = 'pg' if (dbUseSSL && _.isPlainObject(dbConfig)) { dbConfig.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions } break case 'mariadb': case 'mysql': dbClient = 'mysql2' if (dbUseSSL && _.isPlainObject(dbConfig)) { dbConfig.ssl = sslOptions } // Prune host and port if socketPath is configured if (WIKI.config.db.socketPath) { const { host, port, ...prunedConfig } = dbConfig dbConfig = prunedConfig dbConfig.socketPath = WIKI.config.db.socketPath.toString() } // Fix mysql boolean handling... dbConfig.typeCast = (field, next) => { if (field.type === 'TINY' && field.length === 1) { let value = field.string() return value ? (value === '1') : null } return next() } break case 'mssql': dbClient = 'mssql' if (_.isPlainObject(dbConfig)) { dbConfig.appName = 'Wiki.js' _.set(dbConfig, 'options.appName', 'Wiki.js') dbConfig.enableArithAbort = true _.set(dbConfig, 'options.enableArithAbort', true) if (dbUseSSL) { dbConfig.encrypt = true _.set(dbConfig, 'options.encrypt', true) } } break case 'sqlite': dbClient = 'sqlite3' dbConfig = { filename: WIKI.config.db.storage } break default: WIKI.logger.error('Invalid DB Type') process.exit(1) } // Initialize Knex this.knex = Knex({ client: dbClient, useNullAsDefault: true, asyncStackTraces: WIKI.IS_DEBUG, connection: dbConfig, pool: { ...WIKI.config.pool, async afterCreate(conn, done) { // -> Set Connection App Name switch (WIKI.config.db.type) { case 'postgres': await conn.query(`set application_name = 'Wiki.js'`) // -> Set schema if it's not public if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') { await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`) } done() break case 'mysql': await conn.promise().query(`set autocommit = 1`) done() break default: done() break } } }, debug: WIKI.IS_DEBUG }) Objection.Model.knex(this.knex) // Load DB Models const models = autoload(path.join(WIKI.SERVERPATH, 'models')) // Set init tasks let conAttempts = 0 let initTasks = { // -> Attempt initial connection async connect () { try { WIKI.logger.info('Connecting to database...') await self.knex.raw('SELECT 1 + 1;') WIKI.logger.info('Database Connection Successful [ OK ]') } catch (err) { if (conAttempts < 10) { if (err.code) { WIKI.logger.error(`Database Connection Error: ${err.code} ${err.address}:${err.port}`) } else { WIKI.logger.error(`Database Connection Error: ${err.message}`) } WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++conAttempts} of 10]`) await new Promise(resolve => setTimeout(resolve, 3000)) await initTasks.connect() } else { throw err } } }, // -> Migrate DB Schemas async syncSchemas () { return self.knex.migrate.latest({ tableName: 'migrations', migrationSource }) }, // -> Migrate DB Schemas from beta async migrateFromBeta () { return migrateFromBeta.migrate(self.knex) } } let initTasksQueue = (WIKI.IS_MASTER) ? [ initTasks.connect, initTasks.migrateFromBeta, initTasks.syncSchemas ] : [ () => { return Promise.resolve() } ] // Perform init tasks WIKI.logger.info(`Using database driver ${dbClient} for ${WIKI.config.db.type} [ OK ]`) this.onReady = Promise.each(initTasksQueue, t => t()).return(true) return { ...this, ...models } }, /** * Subscribe to database LISTEN / NOTIFY for multi-instances events */ async subscribeToNotifications () { const useHA = (WIKI.config.ha === true || (typeof WIKI.config.ha === 'string' && WIKI.config.ha.toLowerCase() === 'true') || WIKI.config.ha === 1 || WIKI.config.ha === '1') if (!useHA) { return } else if (WIKI.config.db.type !== 'postgres') { WIKI.logger.warn(`Database engine doesn't support pub/sub. Will not handle concurrent instances: [ DISABLED ]`) return } const PGPubSub = require('pg-pubsub') this.listener = new PGPubSub(this.knex.client.connectionSettings, { log (ev) { WIKI.logger.debug(ev) } }) // -> Outbound events handling this.listener.addChannel('wiki', payload => { if (_.has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) { WIKI.logger.info(`Received event ${payload.event} from instance ${payload.source}: [ OK ]`) WIKI.events.inbound.emit(payload.event, payload.value) } }) WIKI.events.outbound.onAny(this.notifyViaDB) // -> Listen to inbound events WIKI.auth.subscribeToEvents() WIKI.configSvc.subscribeToEvents() WIKI.models.pages.subscribeToEvents() WIKI.logger.info(`High-Availability Listener initialized successfully: [ OK ]`) }, /** * Unsubscribe from database LISTEN / NOTIFY */ async unsubscribeToNotifications () { if (this.listener) { WIKI.events.outbound.offAny(this.notifyViaDB) WIKI.events.inbound.removeAllListeners() this.listener.close() } }, /** * Publish event via database NOTIFY * * @param {string} event Event fired * @param {object} value Payload of the event */ notifyViaDB (event, value) { WIKI.models.listener.publish('wiki', { source: WIKI.INSTANCE_ID, event, value }) } } ================================================ FILE: server/core/extensions.js ================================================ const fs = require('fs-extra') const path = require('path') /* global WIKI */ module.exports = { ext: {}, async init () { const extDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/extensions')) WIKI.logger.info(`Checking for installed optional extensions...`) for (let dir of extDirs) { WIKI.extensions.ext[dir] = require(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.js')) const isInstalled = await WIKI.extensions.ext[dir].check() if (isInstalled) { WIKI.logger.info(`Optional extension ${dir} is installed. [ OK ]`) } else { WIKI.logger.info(`Optional extension ${dir} was not found on this system. [ SKIPPED ]`) } } } } ================================================ FILE: server/core/kernel.js ================================================ const _ = require('lodash') const EventEmitter = require('eventemitter2').EventEmitter2 /* global WIKI */ module.exports = { async init() { WIKI.logger.info('=======================================') WIKI.logger.info(`= Wiki.js ${_.padEnd(WIKI.version + ' ', 29, '=')}`) WIKI.logger.info('=======================================') WIKI.logger.info('Initializing...') WIKI.models = require('./db').init() try { await WIKI.models.onReady await WIKI.configSvc.loadFromDb() await WIKI.configSvc.applyFlags() } catch (err) { WIKI.logger.error('Database Initialization Error: ' + err.message) if (WIKI.IS_DEBUG) { WIKI.logger.error(err) } process.exit(1) } this.bootMaster() }, /** * Pre-Master Boot Sequence */ async preBootMaster() { try { await this.initTelemetry() WIKI.sideloader = await require('./sideloader').init() WIKI.cache = require('./cache').init() WIKI.scheduler = require('./scheduler').init() WIKI.servers = require('./servers') WIKI.events = { inbound: new EventEmitter(), outbound: new EventEmitter() } WIKI.extensions = require('./extensions') WIKI.asar = require('./asar') } catch (err) { WIKI.logger.error(err) process.exit(1) } }, /** * Boot Master Process */ async bootMaster() { try { if (WIKI.config.setup) { WIKI.logger.info('Starting setup wizard...') require('../setup')() } else { await this.preBootMaster() await require('../master')() this.postBootMaster() } } catch (err) { WIKI.logger.error(err) process.exit(1) } }, /** * Post-Master Boot Sequence */ async postBootMaster() { await WIKI.models.analytics.refreshProvidersFromDisk() await WIKI.models.authentication.refreshStrategiesFromDisk() await WIKI.models.commentProviders.refreshProvidersFromDisk() await WIKI.models.editors.refreshEditorsFromDisk() await WIKI.models.loggers.refreshLoggersFromDisk() await WIKI.models.renderers.refreshRenderersFromDisk() await WIKI.models.searchEngines.refreshSearchEnginesFromDisk() await WIKI.models.storage.refreshTargetsFromDisk() await WIKI.extensions.init() await WIKI.auth.activateStrategies() await WIKI.models.commentProviders.initProvider() await WIKI.models.searchEngines.initEngine() await WIKI.models.storage.initTargets() WIKI.scheduler.start() await WIKI.models.subscribeToNotifications() }, /** * Init Telemetry */ async initTelemetry() { require('./telemetry').init() process.on('unhandledRejection', (err) => { WIKI.logger.warn(err) WIKI.telemetry.sendError(err) }) process.on('uncaughtException', (err) => { WIKI.logger.warn(err) WIKI.telemetry.sendError(err) }) }, /** * Graceful shutdown */ async shutdown (devMode = false) { if (WIKI.servers) { await WIKI.servers.stopServers() } if (WIKI.scheduler) { await WIKI.scheduler.stop() } if (WIKI.models) { await WIKI.models.unsubscribeToNotifications() if (WIKI.models.knex) { await WIKI.models.knex.destroy() } } if (WIKI.asar) { await WIKI.asar.unload() } if (!devMode) { process.exit(0) } } } ================================================ FILE: server/core/letsencrypt.js ================================================ const ACME = require('acme') const Keypairs = require('@root/keypairs') const _ = require('lodash') const moment = require('moment') const CSR = require('@root/csr') const PEM = require('@root/pem') // eslint-disable-next-line node/no-deprecated-api const punycode = require('punycode') /* global WIKI */ module.exports = { apiDirectory: WIKI.dev ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory', acme: null, async init () { if (!_.get(WIKI.config, 'letsencrypt.payload', false)) { await this.requestCertificate() } else if (WIKI.config.letsencrypt.domain !== WIKI.config.ssl.domain) { WIKI.logger.info(`(LETSENCRYPT) Domain has changed. Requesting new certificates...`) await this.requestCertificate() } else if (moment(WIKI.config.letsencrypt.payload.expires).isSameOrBefore(moment().add(5, 'days'))) { WIKI.logger.info(`(LETSENCRYPT) Certificate is about to or has expired, requesting a new one...`) await this.requestCertificate() } else { WIKI.logger.info(`(LETSENCRYPT) Using existing certificate for ${WIKI.config.ssl.domain}, expires on ${WIKI.config.letsencrypt.payload.expires}: [ OK ]`) } WIKI.config.ssl.format = 'pem' WIKI.config.ssl.inline = true WIKI.config.ssl.key = WIKI.config.letsencrypt.serverKey WIKI.config.ssl.cert = WIKI.config.letsencrypt.payload.cert + '\n' + WIKI.config.letsencrypt.payload.chain WIKI.config.ssl.passphrase = null WIKI.config.ssl.dhparam = null }, async requestCertificate () { try { WIKI.logger.info(`(LETSENCRYPT) Initializing Let's Encrypt client...`) this.acme = ACME.create({ maintainerEmail: WIKI.config.maintainerEmail, packageAgent: `wikijs/${WIKI.version}`, notify: (ev, msg) => { if (_.includes(['warning', 'error'], ev)) { WIKI.logger.warn(`${ev}: ${msg}`) } else { WIKI.logger.debug(`${ev}: ${JSON.stringify(msg)}`) } } }) await this.acme.init(this.apiDirectory) // -> Create ACME Subscriber account if (!_.get(WIKI.config, 'letsencrypt.account', false)) { WIKI.logger.info(`(LETSENCRYPT) Setting up account for the first time...`) const accountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' }) const account = await this.acme.accounts.create({ subscriberEmail: WIKI.config.ssl.subscriberEmail, agreeToTerms: true, accountKey: accountKeypair.private }) WIKI.config.letsencrypt = { accountKeypair: accountKeypair, account: account, domain: WIKI.config.ssl.domain } await WIKI.configSvc.saveToDb(['letsencrypt']) WIKI.logger.info(`(LETSENCRYPT) Account was setup successfully [ OK ]`) } // -> Create Server Keypair if (!WIKI.config.letsencrypt.serverKey) { WIKI.logger.info(`(LETSENCRYPT) Generating server keypairs...`) const serverKeypair = await Keypairs.generate({ kty: 'RSA', format: 'jwk' }) WIKI.config.letsencrypt.serverKey = await Keypairs.export({ jwk: serverKeypair.private }) WIKI.logger.info(`(LETSENCRYPT) Server keypairs generated successfully [ OK ]`) } // -> Create CSR WIKI.logger.info(`(LETSENCRYPT) Generating certificate signing request (CSR)...`) const domains = [ punycode.toASCII(WIKI.config.ssl.domain) ] const serverKey = await Keypairs.import({ pem: WIKI.config.letsencrypt.serverKey }) const csrDer = await CSR.csr({ jwk: serverKey, domains, encoding: 'der' }) const csr = PEM.packBlock({ type: 'CERTIFICATE REQUEST', bytes: csrDer }) WIKI.logger.info(`(LETSENCRYPT) CSR generated successfully [ OK ]`) // -> Verify Domain + Get Certificate WIKI.logger.info(`(LETSENCRYPT) Requesting certificate from Let's Encrypt...`) const certResp = await this.acme.certificates.create({ account: WIKI.config.letsencrypt.account, accountKey: WIKI.config.letsencrypt.accountKeypair.private, csr, domains, challenges: { 'http-01': { init () {}, set (data) { WIKI.logger.info(`(LETSENCRYPT) Setting HTTP challenge for ${data.challenge.hostname}: [ READY ]`) WIKI.config.letsencrypt.challenge = data.challenge WIKI.logger.info(`(LETSENCRYPT) Waiting for challenge to complete...`) return null // <- this is needed, cannot be undefined }, get (data) { return WIKI.config.letsencrypt.challenge }, async remove (data) { WIKI.logger.info(`(LETSENCRYPT) Removing HTTP challenge: [ OK ]`) WIKI.config.letsencrypt.challenge = null return null // <- this is needed, cannot be undefined } } } }) WIKI.logger.info(`(LETSENCRYPT) New certificate received successfully: [ COMPLETED ]`) WIKI.config.letsencrypt.payload = certResp WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain await WIKI.configSvc.saveToDb(['letsencrypt']) } catch (err) { WIKI.logger.warn(`(LETSENCRYPT) ${err}`) throw err } } } ================================================ FILE: server/core/localization.js ================================================ const _ = require('lodash') const dotize = require('dotize') const i18nMW = require('i18next-express-middleware') const i18next = require('i18next') const Promise = require('bluebird') const fs = require('fs-extra') const path = require('path') const yaml = require('js-yaml') /* global WIKI */ module.exports = { engine: null, namespaces: [], init() { this.namespaces = WIKI.data.localeNamespaces this.engine = i18next this.engine.init({ load: 'languageOnly', ns: this.namespaces, defaultNS: 'common', saveMissing: false, lng: WIKI.config.lang.code, fallbackLng: 'en' }) // Load current language + namespaces this.refreshNamespaces(true) return this }, /** * Attach i18n middleware for Express * * @param {Object} app Express Instance */ attachMiddleware (app) { app.use(i18nMW.handle(this.engine)) }, /** * Get all entries for a specific locale and namespace * * @param {String} locale Locale code * @param {String} namespace Namespace */ async getByNamespace(locale, namespace) { if (this.engine.hasResourceBundle(locale, namespace)) { let data = this.engine.getResourceBundle(locale, namespace) return _.map(dotize.convert(data), (value, key) => { return { key, value } }) } else { throw new Error('Invalid locale or namespace') } }, /** * Load entries from the DB for a single locale * * @param {String} locale Locale code * @param {*} opts Additional options */ async loadLocale(locale, opts = { silent: false }) { const res = await WIKI.models.locales.query().findOne('code', locale) if (res) { if (_.isPlainObject(res.strings)) { _.forOwn(res.strings, (data, ns) => { this.namespaces.push(ns) this.engine.addResourceBundle(locale, ns, data, true, true) }) } } else if (!opts.silent) { throw new Error('No such locale in local store.') } // -> Load dev locale files if present if (WIKI.IS_DEBUG) { try { const devEntriesRaw = await fs.readFile(path.join(WIKI.SERVERPATH, `locales/${locale}.yml`), 'utf8') if (devEntriesRaw) { const devEntries = yaml.safeLoad(devEntriesRaw) _.forOwn(devEntries, (data, ns) => { this.namespaces.push(ns) this.engine.addResourceBundle(locale, ns, data, true, true) }) WIKI.logger.info(`Loaded dev locales from ${locale}.yml`) } } catch (err) { // ignore } } }, /** * Reload all namespaces for all active locales from the DB * * @param {Boolean} silent No error on fail */ async refreshNamespaces (silent = false) { await this.loadLocale(WIKI.config.lang.code, { silent }) if (WIKI.config.lang.namespacing) { for (let ns of WIKI.config.lang.namespaces) { await this.loadLocale(ns, { silent }) } } }, /** * Set the active locale * * @param {String} locale Locale code */ async setCurrentLocale(locale) { await Promise.fromCallback(cb => { return this.engine.changeLanguage(locale, cb) }) } } ================================================ FILE: server/core/logger.js ================================================ // const _ = require('lodash') const winston = require('winston') /* global WIKI */ module.exports = { loggers: {}, init(uid) { const loggerFormats = [ winston.format.label({ label: uid }), winston.format.timestamp() ] if (WIKI.config.logFormat === 'json') { loggerFormats.push(winston.format.json()) } else { loggerFormats.push(winston.format.colorize()) loggerFormats.push(winston.format.printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`)) } const logger = winston.createLogger({ level: WIKI.config.logLevel, format: winston.format.combine(...loggerFormats) }) // Init Console (default) logger.add(new winston.transports.Console({ level: WIKI.config.logLevel, prettyPrint: true, colorize: true, silent: false, timestamp: true })) // _.forOwn(_.omitBy(WIKI.config.logging.loggers, s => s.enabled === false), (loggerConfig, loggerKey) => { // let loggerModule = require(`../modules/logging/${loggerKey}`) // loggerModule.init(logger, loggerConfig) // this.loggers[logger.key] = loggerModule // }) return logger } } ================================================ FILE: server/core/mail.js ================================================ const nodemailer = require('nodemailer') const _ = require('lodash') const fs = require('fs-extra') const path = require('path') /* global WIKI */ module.exports = { transport: null, templates: {}, init() { if (_.get(WIKI.config, 'mail.host', '').length > 2) { let conf = { host: WIKI.config.mail.host, port: WIKI.config.mail.port, name: WIKI.config.mail.name, secure: WIKI.config.mail.secure, tls: { rejectUnauthorized: !(WIKI.config.mail.verifySSL === false) } } if (_.get(WIKI.config, 'mail.user', '').length > 1) { conf = { ...conf, auth: { user: WIKI.config.mail.user, pass: WIKI.config.mail.pass } } } if (_.get(WIKI.config, 'mail.useDKIM', false)) { conf = { ...conf, dkim: { domainName: WIKI.config.mail.dkimDomainName, keySelector: WIKI.config.mail.dkimKeySelector, privateKey: WIKI.config.mail.dkimPrivateKey } } } this.transport = nodemailer.createTransport(conf) } else { WIKI.logger.warn('Mail is not setup! Please set the configuration in the administration area!') this.transport = null } return this }, async send(opts) { if (!this.transport) { WIKI.logger.warn('Cannot send email because mail is not setup in the administration area!') throw new WIKI.Error.MailNotConfigured() } await this.loadTemplate(opts.template) return this.transport.sendMail({ headers: { 'x-mailer': 'Wiki.js' }, from: `"${WIKI.config.mail.senderName}" <${WIKI.config.mail.senderEmail}>`, to: opts.to, subject: `${opts.subject} - ${WIKI.config.title}`, text: opts.text, html: _.get(this.templates, opts.template)({ logo: (WIKI.config.logoUrl.startsWith('http') ? '' : WIKI.config.host) + WIKI.config.logoUrl, siteTitle: WIKI.config.title, copyright: WIKI.config.company.length > 0 ? WIKI.config.company : 'Powered by Wiki.js', ...opts.data }) }) }, async loadTemplate(key) { if (_.has(this.templates, key)) { return } const keyKebab = _.kebabCase(key) try { const rawTmpl = await fs.readFile(path.join(WIKI.SERVERPATH, `templates/${keyKebab}.html`), 'utf8') _.set(this.templates, key, _.template(rawTmpl)) } catch (err) { WIKI.logger.warn(err) throw new WIKI.Error.MailTemplateFailed() } } } ================================================ FILE: server/core/scheduler.js ================================================ const moment = require('moment') const childProcess = require('child_process') const _ = require('lodash') const configHelper = require('../helpers/config') /* global WIKI */ class Job { constructor({ name, immediate = false, schedule = 'P1D', repeat = false, worker = false }, queue) { this.queue = queue this.finished = Promise.resolve() this.name = name this.immediate = immediate this.schedule = moment.duration(schedule) this.repeat = repeat this.worker = worker } /** * Start Job * * @param {Object} data Job Data */ start(data) { this.queue.jobs.push(this) if (this.immediate) { this.invoke(data) } else { this.enqueue(data) } } /** * Queue the next job run according to the wait duration * * @param {Object} data Job Data */ enqueue(data) { this.timeout = setTimeout(this.invoke.bind(this), this.schedule.asMilliseconds(), data) } /** * Run the actual job * * @param {Object} data Job Data */ async invoke(data) { try { if (this.worker) { const proc = childProcess.fork(`server/core/worker.js`, [ `--job=${this.name}`, `--data=${data}` ], { cwd: WIKI.ROOTPATH, stdio: ['inherit', 'inherit', 'pipe', 'ipc'] }) const stderr = [] proc.stderr.on('data', chunk => stderr.push(chunk)) this.finished = new Promise((resolve, reject) => { proc.on('exit', (code, signal) => { const data = Buffer.concat(stderr).toString() if (code === 0) { resolve(data) } else { const err = new Error(`Error when running job ${this.name}: ${data}`) err.exitSignal = signal err.exitCode = code err.stderr = data reject(err) } proc.kill() }) }) } else { this.finished = require(`../jobs/${this.name}`)(data) } await this.finished } catch (err) { WIKI.logger.warn(err) } if (this.repeat && this.queue.jobs.includes(this)) { this.enqueue(data) } else { this.stop().catch(() => {}) } } /** * Stop any future job invocation from occuring */ async stop() { clearTimeout(this.timeout) this.queue.jobs = this.queue.jobs.filter(x => x !== this) return this.finished } } module.exports = { jobs: [], init() { return this }, start() { _.forOwn(WIKI.data.jobs, (queueParams, queueName) => { if (WIKI.config.offline && queueParams.offlineSkip) { WIKI.logger.warn(`Skipping job ${queueName} because offline mode is enabled. [SKIPPED]`) return } const schedule = (configHelper.isValidDurationString(queueParams.schedule)) ? queueParams.schedule : 'P1D' this.registerJob({ name: _.kebabCase(queueName), immediate: _.get(queueParams, 'onInit', false), schedule: schedule, repeat: _.get(queueParams, 'repeat', false), worker: _.get(queueParams, 'worker', false) }) }) }, registerJob(opts, data) { const job = new Job(opts, this) job.start(data) return job }, async stop() { return Promise.all(this.jobs.map(job => job.stop())) } } ================================================ FILE: server/core/servers.js ================================================ const fs = require('fs-extra') const http = require('http') const https = require('https') const { ApolloServer } = require('apollo-server-express') const Promise = require('bluebird') const _ = require('lodash') const jwt = require('jsonwebtoken') const cookie = require('cookie') /* global WIKI */ module.exports = { servers: { graph: null, http: null, https: null }, connections: new Map(), le: null, /** * Start HTTP Server */ async startHTTP () { WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`) this.servers.http = http.createServer(WIKI.app) this.servers.graph.installSubscriptionHandlers(this.servers.http) this.servers.http.listen(WIKI.config.port, WIKI.config.bindIP) this.servers.http.on('error', (error) => { if (error.syscall !== 'listen') { throw error } switch (error.code) { case 'EACCES': WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!') return process.exit(1) case 'EADDRINUSE': WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!') return process.exit(1) default: throw error } }) this.servers.http.on('listening', () => { WIKI.logger.info('HTTP Server: [ RUNNING ]') }) this.servers.http.on('connection', conn => { let connKey = `http:${conn.remoteAddress}:${conn.remotePort}` this.connections.set(connKey, conn) conn.on('close', () => { this.connections.delete(connKey) }) }) }, /** * Start HTTPS Server */ async startHTTPS () { if (WIKI.config.ssl.provider === 'letsencrypt') { this.le = require('./letsencrypt') await this.le.init() } WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.ssl.port} ]`) const tlsOpts = {} try { if (WIKI.config.ssl.format === 'pem') { tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : fs.readFileSync(WIKI.config.ssl.key) tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : fs.readFileSync(WIKI.config.ssl.cert) } else { tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : fs.readFileSync(WIKI.config.ssl.pfx) } if (!_.isEmpty(WIKI.config.ssl.passphrase)) { tlsOpts.passphrase = WIKI.config.ssl.passphrase } if (!_.isEmpty(WIKI.config.ssl.dhparam)) { tlsOpts.dhparam = WIKI.config.ssl.dhparam } } catch (err) { WIKI.logger.error('Failed to setup HTTPS server parameters:') WIKI.logger.error(err) return process.exit(1) } this.servers.https = https.createServer(tlsOpts, WIKI.app) this.servers.graph.installSubscriptionHandlers(this.servers.https) this.servers.https.listen(WIKI.config.ssl.port, WIKI.config.bindIP) this.servers.https.on('error', (error) => { if (error.syscall !== 'listen') { throw error } switch (error.code) { case 'EACCES': WIKI.logger.error('Listening on port ' + WIKI.config.ssl.port + ' requires elevated privileges!') return process.exit(1) case 'EADDRINUSE': WIKI.logger.error('Port ' + WIKI.config.ssl.port + ' is already in use!') return process.exit(1) default: throw error } }) this.servers.https.on('listening', () => { WIKI.logger.info('HTTPS Server: [ RUNNING ]') }) this.servers.https.on('connection', conn => { let connKey = `https:${conn.remoteAddress}:${conn.remotePort}` this.connections.set(connKey, conn) conn.on('close', () => { this.connections.delete(connKey) }) }) }, /** * Start GraphQL Server */ async startGraphQL () { const graphqlSchema = require('../graph') this.servers.graph = new ApolloServer({ ...graphqlSchema, context: ({ req, res }) => ({ req, res }), subscriptions: { onConnect: (connectionParams, webSocket) => { let token = _.get(connectionParams, 'token', null) if (!token) { const cookieHeader = _.get(webSocket, 'upgradeReq.headers.cookie', '') if (cookieHeader) { const cookies = cookie.parse(cookieHeader) token = cookies.jwt || null } } if (!token) { throw new Error('Unauthorized') } try { const user = jwt.verify(token, WIKI.config.certs.public, { audience: WIKI.config.auth.audience, issuer: 'urn:wiki.js', algorithms: ['RS256'] }) if (!_.includes(user.permissions, 'manage:system')) { throw new Error('Forbidden') } return { user } } catch (err) { throw new Error('Unauthorized') } }, path: '/graphql-subscriptions' } }) this.servers.graph.applyMiddleware({ app: WIKI.app, cors: false }) }, /** * Close all active connections */ closeConnections (mode = 'all') { for (const [key, conn] of this.connections) { if (mode !== `all` && key.indexOf(`${mode}:`) !== 0) { continue } conn.destroy() this.connections.delete(key) } if (mode === 'all') { this.connections.clear() } }, /** * Stop all servers */ async stopServers () { this.closeConnections() if (this.servers.http) { await Promise.fromCallback(cb => { this.servers.http.close(cb) }) this.servers.http = null } if (this.servers.https) { await Promise.fromCallback(cb => { this.servers.https.close(cb) }) this.servers.https = null } this.servers.graph = null }, /** * Restart Server */ async restartServer (srv = 'https') { this.closeConnections(srv) switch (srv) { case 'http': if (this.servers.http) { await Promise.fromCallback(cb => { this.servers.http.close(cb) }) this.servers.http = null } this.startHTTP() break case 'https': if (this.servers.https) { await Promise.fromCallback(cb => { this.servers.https.close(cb) }) this.servers.https = null } this.startHTTPS() break default: throw new Error('Cannot restart server: Invalid designation') } } } ================================================ FILE: server/core/sideloader.js ================================================ const fs = require('fs-extra') const path = require('path') const _ = require('lodash') /* global WIKI */ module.exports = { async init () { if (!WIKI.config.offline) { return } const sideloadExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload')) if (!sideloadExists) { return } WIKI.logger.info('Sideload directory detected. Looking for packages...') try { await this.importLocales() } catch (err) { WIKI.logger.warn(err) } }, async importLocales() { const localeExists = await fs.pathExists(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json')) if (localeExists) { WIKI.logger.info('Found locales master file. Importing locale packages...') let importedLocales = 0 const locales = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'sideload/locales.json')) if (locales && _.has(locales, 'data.localization.locales')) { for (const locale of locales.data.localization.locales) { try { const localeData = await fs.readJson(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `sideload/${locale.code}.json`)) if (localeData) { WIKI.logger.info(`Importing ${locale.name} locale package...`) let lcObj = {} _.forOwn(localeData, (value, key) => { if (_.includes(key, '::')) { return } if (_.isEmpty(value)) { value = key } _.set(lcObj, key.replace(':', '.'), value) }) const localeDbExists = await WIKI.models.locales.query().select('code').where('code', locale.code).first() if (localeDbExists) { await WIKI.models.locales.query().update({ code: locale.code, strings: lcObj, isRTL: locale.isRTL, name: locale.name, nativeName: locale.nativeName, availability: locale.availability || 0 }).where('code', locale.code) } else { await WIKI.models.locales.query().insert({ code: locale.code, strings: lcObj, isRTL: locale.isRTL, name: locale.name, nativeName: locale.nativeName, availability: locale.availability || 0 }) } importedLocales++ } } catch (err) { // skip } } WIKI.logger.info(`Imported ${importedLocales} locale packages: [COMPLETED]`) } } } } ================================================ FILE: server/core/system.js ================================================ const _ = require('lodash') const cfgHelper = require('../helpers/config') const Promise = require('bluebird') const fs = require('fs-extra') const path = require('path') const zlib = require('zlib') const { pipeline } = require('node:stream/promises') const { Readable, Transform } = require('node:stream') /* global WIKI */ module.exports = { updates: { channel: 'BETA', version: WIKI.version, releaseDate: WIKI.releaseDate, minimumVersionRequired: '2.0.0-beta.0', minimumNodeRequired: '10.12.0' }, exportStatus: { status: 'notrunning', progress: 0, message: '', updatedAt: null }, init() { // Clear content cache fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache')) return this }, /** * Upgrade from WIKI.js 1.x - MongoDB database * * @param {Object} opts Options object */ async upgradeFromMongo (opts) { WIKI.logger.info('Upgrading from MongoDB...') let mongo = require('mongodb').MongoClient let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr) return new Promise((resolve, reject) => { // Connect to MongoDB mongo.connect(parsedMongoConStr, { autoReconnect: false, reconnectTries: 2, reconnectInterval: 1000, connectTimeoutMS: 5000, socketTimeoutMS: 5000 }, async (err, db) => { try { if (err !== null) { throw err } let users = db.collection('users') // Check if users table is populated let userCount = await users.count() if (userCount < 2) { throw new Error('MongoDB Upgrade: Users table is empty!') } // Import all users let userData = await users.find({ email: { $not: 'guest' } }).toArray() await WIKI.models.User.bulkCreate(_.map(userData, usr => { return { email: usr.email, name: usr.name || 'Imported User', password: usr.password || '', provider: usr.provider || 'local', providerId: usr.providerId || '', role: 'user', createdAt: usr.createdAt } })) resolve(true) } catch (errc) { reject(errc) } db.close() }) }) }, /** * Export Wiki to Disk */ async export (opts) { this.exportStatus.status = 'running' this.exportStatus.progress = 0 this.exportStatus.message = '' this.exportStatus.startedAt = new Date() WIKI.logger.info(`Export started to path ${opts.path}`) WIKI.logger.info(`Entities to export: ${opts.entities.join(', ')}`) const progressMultiplier = 1 / opts.entities.length try { for (const entity of opts.entities) { switch (entity) { // ----------------------------------------- // ASSETS // ----------------------------------------- case 'assets': { WIKI.logger.info('Exporting assets...') const assetFolders = await WIKI.models.assetFolders.getAllPaths() const assetsCountRaw = await WIKI.models.assets.query().count('* as total').first() const assetsCount = parseInt(assetsCountRaw.total) if (assetsCount < 1) { WIKI.logger.warn('There are no assets to export! Skipping...') break } const assetsProgressMultiplier = progressMultiplier / Math.ceil(assetsCount / 50) WIKI.logger.info(`Found ${assetsCount} assets to export. Streaming to disk...`) await pipeline( WIKI.models.knex.select('filename', 'folderId', 'data').from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`Exporting asset ${filename}...`) await fs.outputFile(path.join(opts.path, 'assets', filename), asset.data) this.exportStatus.progress += assetsProgressMultiplier * 100 cb() } }) ) WIKI.logger.info('Export: assets saved to disk successfully.') break } // ----------------------------------------- // COMMENTS // ----------------------------------------- case 'comments': { WIKI.logger.info('Exporting comments...') const outputPath = path.join(opts.path, 'comments.json.gz') const commentsCountRaw = await WIKI.models.comments.query().count('* as total').first() const commentsCount = parseInt(commentsCountRaw.total) if (commentsCount < 1) { WIKI.logger.warn('There are no comments to export! Skipping...') break } const commentsProgressMultiplier = progressMultiplier / Math.ceil(commentsCount / 50) WIKI.logger.info(`Found ${commentsCount} comments to export. Streaming to file...`) const rs = Readable({ objectMode: true }) rs._read = () => {} const fetchCommentsBatch = async (offset) => { const comments = await WIKI.models.comments.query().offset(offset).limit(50).withGraphJoined({ author: true, page: true }).modifyGraph('author', builder => { builder.select('users.id', 'users.name', 'users.email', 'users.providerKey') }).modifyGraph('page', builder => { builder.select('pages.id', 'pages.path', 'pages.localeCode', 'pages.title') }) if (comments.length > 0) { for (const cmt of comments) { rs.push(cmt) } fetchCommentsBatch(offset + 50) } else { rs.push(null) } this.exportStatus.progress += commentsProgressMultiplier * 100 } fetchCommentsBatch(0) let marker = 0 await pipeline( rs, new Transform({ objectMode: true, transform (chunk, encoding, callback) { marker++ let outputStr = marker === 1 ? '[\n' : '' outputStr += JSON.stringify(chunk, null, 2) if (marker < commentsCount) { outputStr += ',\n' } callback(null, outputStr) }, flush (callback) { callback(null, '\n]') } }), zlib.createGzip(), fs.createWriteStream(outputPath) ) WIKI.logger.info('Export: comments.json.gz created successfully.') break } // ----------------------------------------- // GROUPS // ----------------------------------------- case 'groups': { WIKI.logger.info('Exporting groups...') const outputPath = path.join(opts.path, 'groups.json') const groups = await WIKI.models.groups.query() await fs.outputJSON(outputPath, groups, { spaces: 2 }) WIKI.logger.info('Export: groups.json created successfully.') this.exportStatus.progress += progressMultiplier * 100 break } // ----------------------------------------- // HISTORY // ----------------------------------------- case 'history': { WIKI.logger.info('Exporting pages history...') const outputPath = path.join(opts.path, 'pages-history.json.gz') const pagesCountRaw = await WIKI.models.pageHistory.query().count('* as total').first() const pagesCount = parseInt(pagesCountRaw.total) if (pagesCount < 1) { WIKI.logger.warn('There are no pages history to export! Skipping...') break } const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10) WIKI.logger.info(`Found ${pagesCount} pages history to export. Streaming to file...`) const rs = Readable({ objectMode: true }) rs._read = () => {} const fetchPagesBatch = async (offset) => { const pages = await WIKI.models.pageHistory.query().offset(offset).limit(10).withGraphJoined({ author: true, page: true, tags: true }).modifyGraph('author', builder => { builder.select('users.id', 'users.name', 'users.email', 'users.providerKey') }).modifyGraph('page', builder => { builder.select('pages.id', 'pages.title', 'pages.path', 'pages.localeCode') }).modifyGraph('tags', builder => { builder.select('tags.tag', 'tags.title') }) if (pages.length > 0) { for (const page of pages) { rs.push(page) } fetchPagesBatch(offset + 10) } else { rs.push(null) } this.exportStatus.progress += pagesProgressMultiplier * 100 } fetchPagesBatch(0) let marker = 0 await pipeline( rs, new Transform({ objectMode: true, transform (chunk, encoding, callback) { marker++ let outputStr = marker === 1 ? '[\n' : '' outputStr += JSON.stringify(chunk, null, 2) if (marker < pagesCount) { outputStr += ',\n' } callback(null, outputStr) }, flush (callback) { callback(null, '\n]') } }), zlib.createGzip(), fs.createWriteStream(outputPath) ) WIKI.logger.info('Export: pages-history.json.gz created successfully.') break } // ----------------------------------------- // NAVIGATION // ----------------------------------------- case 'navigation': { WIKI.logger.info('Exporting navigation...') const outputPath = path.join(opts.path, 'navigation.json') const navigationRaw = await WIKI.models.navigation.query() const navigation = navigationRaw.reduce((obj, cur) => { obj[cur.key] = cur.config return obj }, {}) await fs.outputJSON(outputPath, navigation, { spaces: 2 }) WIKI.logger.info('Export: navigation.json created successfully.') this.exportStatus.progress += progressMultiplier * 100 break } // ----------------------------------------- // PAGES // ----------------------------------------- case 'pages': { WIKI.logger.info('Exporting pages...') const outputPath = path.join(opts.path, 'pages.json.gz') const pagesCountRaw = await WIKI.models.pages.query().count('* as total').first() const pagesCount = parseInt(pagesCountRaw.total) if (pagesCount < 1) { WIKI.logger.warn('There are no pages to export! Skipping...') break } const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10) WIKI.logger.info(`Found ${pagesCount} pages to export. Streaming to file...`) const rs = Readable({ objectMode: true }) rs._read = () => {} const fetchPagesBatch = async (offset) => { const pages = await WIKI.models.pages.query().offset(offset).limit(10).withGraphJoined({ author: true, creator: true, tags: true }).modifyGraph('author', builder => { builder.select('users.id', 'users.name', 'users.email', 'users.providerKey') }).modifyGraph('creator', builder => { builder.select('users.id', 'users.name', 'users.email', 'users.providerKey') }).modifyGraph('tags', builder => { builder.select('tags.tag', 'tags.title') }) if (pages.length > 0) { for (const page of pages) { rs.push(page) } fetchPagesBatch(offset + 10) } else { rs.push(null) } this.exportStatus.progress += pagesProgressMultiplier * 100 } fetchPagesBatch(0) let marker = 0 await pipeline( rs, new Transform({ objectMode: true, transform (chunk, encoding, callback) { marker++ let outputStr = marker === 1 ? '[\n' : '' outputStr += JSON.stringify(chunk, null, 2) if (marker < pagesCount) { outputStr += ',\n' } callback(null, outputStr) }, flush (callback) { callback(null, '\n]') } }), zlib.createGzip(), fs.createWriteStream(outputPath) ) WIKI.logger.info('Export: pages.json.gz created successfully.') break } // ----------------------------------------- // SETTINGS // ----------------------------------------- case 'settings': { WIKI.logger.info('Exporting settings...') const outputPath = path.join(opts.path, 'settings.json') const config = { ...WIKI.config, modules: { analytics: await WIKI.models.analytics.query(), authentication: (await WIKI.models.authentication.query()).map(a => ({ ...a, domainWhitelist: _.get(a, 'domainWhitelist.v', []), autoEnrollGroups: _.get(a, 'autoEnrollGroups.v', []) })), commentProviders: await WIKI.models.commentProviders.query(), renderers: await WIKI.models.renderers.query(), searchEngines: await WIKI.models.searchEngines.query(), storage: await WIKI.models.storage.query() }, apiKeys: await WIKI.models.apiKeys.query().where('isRevoked', false) } await fs.outputJSON(outputPath, config, { spaces: 2 }) WIKI.logger.info('Export: settings.json created successfully.') this.exportStatus.progress += progressMultiplier * 100 break } // ----------------------------------------- // USERS // ----------------------------------------- case 'users': { WIKI.logger.info('Exporting users...') const outputPath = path.join(opts.path, 'users.json.gz') const usersCountRaw = await WIKI.models.users.query().count('* as total').first() const usersCount = parseInt(usersCountRaw.total) if (usersCount < 1) { WIKI.logger.warn('There are no users to export! Skipping...') break } const usersProgressMultiplier = progressMultiplier / Math.ceil(usersCount / 50) WIKI.logger.info(`Found ${usersCount} users to export. Streaming to file...`) const rs = Readable({ objectMode: true }) rs._read = () => {} const fetchUsersBatch = async (offset) => { const users = await WIKI.models.users.query().offset(offset).limit(50).withGraphJoined({ groups: true, provider: true }).modifyGraph('groups', builder => { builder.select('groups.id', 'groups.name') }).modifyGraph('provider', builder => { builder.select('authentication.key', 'authentication.strategyKey', 'authentication.displayName') }) if (users.length > 0) { for (const usr of users) { rs.push(usr) } fetchUsersBatch(offset + 50) } else { rs.push(null) } this.exportStatus.progress += usersProgressMultiplier * 100 } fetchUsersBatch(0) let marker = 0 await pipeline( rs, new Transform({ objectMode: true, transform (chunk, encoding, callback) { marker++ let outputStr = marker === 1 ? '[\n' : '' outputStr += JSON.stringify(chunk, null, 2) if (marker < usersCount) { outputStr += ',\n' } callback(null, outputStr) }, flush (callback) { callback(null, '\n]') } }), zlib.createGzip(), fs.createWriteStream(outputPath) ) WIKI.logger.info('Export: users.json.gz created successfully.') break } } } this.exportStatus.status = 'success' this.exportStatus.progress = 100 } catch (err) { this.exportStatus.status = 'error' this.exportStatus.message = err.message } } } ================================================ FILE: server/core/telemetry.js ================================================ const _ = require('lodash') const { createApolloFetch } = require('apollo-fetch') const { v4: uuid } = require('uuid') const os = require('os') const fs = require('fs-extra') /* global WIKI */ module.exports = { enabled: false, init() { WIKI.telemetry = this if (_.get(WIKI.config, 'telemetry.isEnabled', false) === true && WIKI.config.offline !== true) { this.enabled = true this.sendInstanceEvent('STARTUP') } }, sendError(err) { // TODO }, sendEvent(eventCategory, eventAction, eventLabel) { // TODO }, async sendInstanceEvent(eventType) { if (WIKI.devMode || !this.enabled) { return } try { const apollo = createApolloFetch({ uri: WIKI.config.graphEndpoint }) // Platform detection let platform = 'LINUX' let isDockerized = false let osname = `${os.type()} ${os.release()}` switch (os.platform()) { case 'win32': platform = 'WINDOWS' break case 'darwin': platform = 'MACOS' break default: platform = 'LINUX' isDockerized = await fs.pathExists('/.dockerenv') if (isDockerized) { osname = 'Docker' } break } // DB Version detection let dbVersion = 'Unknown' switch (WIKI.config.db.type) { case 'mariadb': case 'mysql': const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;') dbVersion = _.get(resultMYSQL, '[0][0].version', 'Unknown') break case 'mssql': const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;') dbVersion = _.get(resultMSSQL, '[0].version', 'Unknown') break case 'postgres': dbVersion = _.get(WIKI.models, 'knex.client.version', 'Unknown') break case 'sqlite': dbVersion = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown') break } let arch = os.arch().toUpperCase() if (['ARM', 'ARM64', 'X32', 'X64'].indexOf(arch) < 0) { arch = 'OTHER' } // Send Event const respStrings = await apollo({ query: `mutation ( $version: String! $platform: TelemetryPlatform! $os: String! $architecture: TelemetryArchitecture! $dbType: TelemetryDBType! $dbVersion: String! $nodeVersion: String! $cpuCores: Int! $ramMBytes: Int!, $clientId: String!, $event: TelemetryInstanceEvent! ) { telemetry { instance( version: $version platform: $platform os: $os architecture: $architecture dbType: $dbType dbVersion: $dbVersion nodeVersion: $nodeVersion cpuCores: $cpuCores ramMBytes: $ramMBytes clientId: $clientId event: $event ) { responseResult { succeeded errorCode slug message } } } }`, variables: { version: WIKI.version, platform, os: osname, architecture: arch, dbType: WIKI.config.db.type.toUpperCase(), dbVersion, nodeVersion: process.version.substr(1), cpuCores: os.cpus().length, ramMBytes: Math.round(os.totalmem() / 1024 / 1024), clientId: WIKI.config.telemetry.clientId, event: eventType } }) const telemetryResponse = _.get(respStrings, 'data.telemetry.instance.responseResult', { succeeded: false, message: 'Unexpected Error' }) if (!telemetryResponse.succeeded) { WIKI.logger.warn('Failed to send instance telemetry: ' + telemetryResponse.message) } else { WIKI.logger.info('Telemetry is active: [ OK ]') } } catch (err) { WIKI.logger.warn(err) } }, generateClientId() { _.set(WIKI.config, 'telemetry.clientId', uuid()) return WIKI.config.telemetry.clientId } } ================================================ FILE: server/core/worker.js ================================================ const path = require('path') let WIKI = { IS_DEBUG: process.env.NODE_ENV === 'development', ROOTPATH: process.cwd(), SERVERPATH: path.join(process.cwd(), 'server'), Error: require('../helpers/error'), configSvc: require('./config') } global.WIKI = WIKI WIKI.configSvc.init() WIKI.logger = require('./logger').init('JOB') const args = require('yargs').argv ;(async () => { try { await require(`../jobs/${args.job}`)(args.data) process.exit(0) } catch (e) { await new Promise(resolve => process.stderr.write(e.message, resolve)) process.exit(1) } })() ================================================ FILE: server/db/beta/index.js ================================================ const _ = require('lodash') const path = require('path') const fs = require('fs-extra') const semver = require('semver') /* global WIKI */ module.exports = { async migrate (knex) { const migrationsTableExists = await knex.schema.hasTable('migrations') if (!migrationsTableExists) { return } const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } const migrations = await knex('migrations') if (_.some(migrations, m => m.name.indexOf('2.0.0-beta') >= 0)) { // -> Pre-beta.241 locale field length fix const localeColnInfo = await knex('pages').columnInfo('localeCode') if (WIKI.config.db.type !== 'sqlite' && localeColnInfo.maxLength === 2) { // -> Load locales const locales = await knex('locales') await knex.schema // -> Remove constraints .table('users', table => { table.dropForeign('localeCode') }) .table('pages', table => { table.dropForeign('localeCode') }) .table('pageHistory', table => { table.dropForeign('localeCode') }) .table('pageTree', table => { table.dropForeign('localeCode') }) // -> Recreate locales table .dropTable('locales') .createTable('locales', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('code', 5).notNullable().primary() table.json('strings') table.boolean('isRTL').notNullable().defaultTo(false) table.string('name').notNullable() table.string('nativeName').notNullable() table.integer('availability').notNullable().defaultTo(0) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) await knex('locales').insert(locales) // -> Alter columns length await knex.schema .table('users', table => { table.string('localeCode', 5).notNullable().defaultTo('en').alter() }) .table('pages', table => { table.string('localeCode', 5).alter() }) .table('pageHistory', table => { table.string('localeCode', 5).alter() }) .table('pageTree', table => { table.string('localeCode', 5).alter() }) // -> Restore restraints .table('users', table => { table.foreign('localeCode').references('code').inTable('locales') }) .table('pages', table => { table.foreign('localeCode').references('code').inTable('locales') }) .table('pageHistory', table => { table.foreign('localeCode').references('code').inTable('locales') }) .table('pageTree', table => { table.foreign('localeCode').references('code').inTable('locales') }) } // -> Advance to latest beta/rc migration state const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/beta/migrations' : 'db/beta/migrations-sqlite') await knex.migrate.latest({ tableName: 'migrations', migrationSource: { async getMigrations() { const migrationFiles = await fs.readdir(baseMigrationPath) return migrationFiles.sort(semver.compare).map(m => ({ file: m, directory: baseMigrationPath })) }, getMigrationName(migration) { return migration.file }, getMigration(migration) { return require(path.join(baseMigrationPath, migration.file)) } } }) // -> Cleanup migration table await knex('migrations').truncate() // -> Advance to stable 2.0 migration state await knex('migrations').insert({ name: '2.0.0.js', batch: 1, migration_time: knex.fn.now() }) } } } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.1.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema // ===================================== // MODEL TABLES // ===================================== // ASSETS ------------------------------ .createTable('assets', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('filename').notNullable() table.string('basename').notNullable() table.string('ext').notNullable() table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary') table.string('mime').notNullable().defaultTo('application/octet-stream') table.integer('fileSize').unsigned().comment('In kilobytes') table.json('metadata') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // ASSET FOLDERS ----------------------- .createTable('assetFolders', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('name').notNullable() table.string('slug').notNullable() table.integer('parentId').unsigned().references('id').inTable('assetFolders') }) // AUTHENTICATION ---------------------- .createTable('authentication', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() table.boolean('selfRegistration').notNullable().defaultTo(false) table.json('domainWhitelist').notNullable() table.json('autoEnrollGroups').notNullable() }) // COMMENTS ---------------------------- .createTable('comments', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.text('content').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // EDITORS ----------------------------- .createTable('editors', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // GROUPS ------------------------------ .createTable('groups', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('name').notNullable() table.json('permissions').notNullable() table.json('pageRules').notNullable() table.boolean('isSystem').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOCALES ----------------------------- .createTable('locales', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('code', 2).notNullable().primary() table.json('strings') table.boolean('isRTL').notNullable().defaultTo(false) table.string('name').notNullable() table.string('nativeName').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOGGING ---------------------------- .createTable('loggers', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('level').notNullable().defaultTo('warn') table.json('config') }) // NAVIGATION ---------------------------- .createTable('navigation', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.json('config') }) // PAGE HISTORY ------------------------ .createTable('pageHistory', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() }) // PAGES ------------------------------- .createTable('pages', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('privateNS') table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.text('render') table.json('toc') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // PAGE TREE --------------------------- .createTable('pageTree', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') }) // RENDERERS --------------------------- .createTable('renderers', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SEARCH ------------------------------ .createTable('searchEngines', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SETTINGS ---------------------------- .createTable('settings', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.json('value') table.string('updatedAt').notNullable() }) // STORAGE ----------------------------- .createTable('storage', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push') table.json('config') }) // TAGS -------------------------------- .createTable('tags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('tag').notNullable().unique() table.string('title') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // USER KEYS --------------------------- .createTable('userKeys', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('kind').notNullable() table.string('token').notNullable() table.string('createdAt').notNullable() table.string('validUntil').notNullable() }) // USERS ------------------------------- .createTable('users', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('email').notNullable() table.string('name').notNullable() table.string('providerId') table.string('password') table.boolean('tfaIsActive').notNullable().defaultTo(false) table.string('tfaSecret') table.string('jobTitle').defaultTo('') table.string('location').defaultTo('') table.string('pictureUrl') table.string('timezone').notNullable().defaultTo('America/New_York') table.boolean('isSystem').notNullable().defaultTo(false) table.boolean('isActive').notNullable().defaultTo(false) table.boolean('isVerified').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // ===================================== // RELATION TABLES // ===================================== // PAGE HISTORY TAGS --------------------------- .createTable('pageHistoryTags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // PAGE TAGS --------------------------- .createTable('pageTags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // USER GROUPS ------------------------- .createTable('userGroups', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE') table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE') }) // ===================================== // REFERENCES // ===================================== .table('assets', table => { table.integer('folderId').unsigned().references('id').inTable('assetFolders') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('comments', table => { table.integer('pageId').unsigned().references('id').inTable('pages') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('pageHistory', table => { table.integer('pageId').unsigned().references('id').inTable('pages') table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 2).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('pages', table => { table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 2).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') table.integer('creatorId').unsigned().references('id').inTable('users') }) .table('pageTree', table => { table.integer('parent').unsigned().references('id').inTable('pageTree') table.integer('pageId').unsigned().references('id').inTable('pages') table.string('localeCode', 2).references('code').inTable('locales') }) .table('userKeys', table => { table.integer('userId').unsigned().references('id').inTable('users') }) .table('users', table => { table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') table.string('localeCode', 2).references('code').inTable('locales').notNullable().defaultTo('en') table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown') table.unique(['providerKey', 'email']) }) } exports.down = knex => { return knex.schema .dropTableIfExists('userGroups') .dropTableIfExists('pageHistoryTags') .dropTableIfExists('pageHistory') .dropTableIfExists('pageTags') .dropTableIfExists('assets') .dropTableIfExists('assetFolders') .dropTableIfExists('comments') .dropTableIfExists('editors') .dropTableIfExists('groups') .dropTableIfExists('locales') .dropTableIfExists('navigation') .dropTableIfExists('pages') .dropTableIfExists('renderers') .dropTableIfExists('settings') .dropTableIfExists('storage') .dropTableIfExists('tags') .dropTableIfExists('userKeys') .dropTableIfExists('users') } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.11.js ================================================ exports.up = knex => { return knex.schema .table('pageHistory', table => { table.string('action').defaultTo('updated') table.dropForeign('pageId') }) } exports.down = knex => { return knex.schema .table('pageHistory', table => { table.dropColumn('action') table.integer('pageId').unsigned().references('id').inTable('pages') }) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.127.js ================================================ exports.up = knex => { return knex.schema .table('assets', table => { table.dropColumn('basename') table.string('hash').notNullable() }) } exports.down = knex => { return knex.schema .table('assets', table => { table.dropColumn('hash') table.string('basename').notNullable() }) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.148.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .table('assetData', table => { if (dbCompat.blobLength) { table.dropColumn('data') } }) .table('assetData', table => { if (dbCompat.blobLength) { table.specificType('data', 'LONGBLOB').notNullable() } }) } exports.down = knex => { return knex.schema .table('assetData', table => {}) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.205.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('analytics', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) } exports.down = knex => { return knex.schema .dropTableIfExists('analytics') } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.217.js ================================================ exports.up = knex => { return knex.schema .table('locales', table => { table.integer('availability').notNullable().defaultTo(0) }) } exports.down = knex => { return knex.schema .table('locales', table => { table.dropColumn('availability') }) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.242.js ================================================ exports.up = knex => { return knex.schema .table('users', table => { table.boolean('mustChangePwd').notNullable().defaultTo(false) }) } exports.down = knex => { return knex.schema .table('users', table => { table.dropColumn('mustChangePwd') }) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.293.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('pageLinks', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('path').notNullable() table.string('localeCode', 5).notNullable() }) .table('pageLinks', table => { table.index(['path', 'localeCode']) }) } exports.down = knex => { return knex.schema .dropTableIfExists('pageLinks') } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.38.js ================================================ exports.up = knex => { return knex.schema .table('storage', table => { table.string('syncInterval') table.json('state') }) } exports.down = knex => { return knex.schema .table('storage', table => { table.dropColumn('syncInterval') table.dropColumn('state') }) } ================================================ FILE: server/db/beta/migrations/2.0.0-beta.99.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('assetData', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').primary() table.binary('data').notNullable() }) } exports.down = knex => { return knex.schema .dropTableIfExists('assetData') } ================================================ FILE: server/db/beta/migrations/2.0.0-rc.2.js ================================================ /* global WIKI */ exports.up = async knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), selfCascadeDelete: WIKI.config.db.type !== 'mssql' } return knex.schema .dropTable('pageTree') .createTable('pageTree', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').unsigned().primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') }) .table('pageTree', table => { if (dbCompat.selfCascadeDelete) { table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE') } else { table.integer('parent').unsigned() } table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('localeCode', 5).references('code').inTable('locales') }) } exports.down = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), selfCascadeDelete: WIKI.config.db.type !== 'mssql' } return knex.schema .dropTable('pageTree') .createTable('pageTree', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') }) .table('pageTree', table => { table.integer('parent').unsigned().references('id').inTable('pageTree') table.integer('pageId').unsigned().references('id').inTable('pages') table.string('localeCode', 5).references('code').inTable('locales') }) } ================================================ FILE: server/db/beta/migrations/2.0.0-rc.29.js ================================================ /* global WIKI */ exports.up = knex => { return knex.schema .table('pages', table => { switch (WIKI.config.db.type) { case 'mariadb': case 'mysql': table.specificType('content', 'LONGTEXT').alter() table.specificType('render', 'LONGTEXT').alter() break case 'mssql': table.specificType('content', 'VARCHAR(max)').alter() table.specificType('render', 'VARCHAR(max)').alter() break } }) } exports.down = knex => { } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.1.js ================================================ exports.up = knex => { return knex.schema // ===================================== // MODEL TABLES // ===================================== // ASSETS ------------------------------ .createTable('assets', table => { table.increments('id').primary() table.string('filename').notNullable() table.string('basename').notNullable() table.string('ext').notNullable() table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary') table.string('mime').notNullable().defaultTo('application/octet-stream') table.integer('fileSize').unsigned().comment('In kilobytes') table.json('metadata') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.integer('folderId').unsigned().references('id').inTable('assetFolders') table.integer('authorId').unsigned().references('id').inTable('users') }) // ASSET FOLDERS ----------------------- .createTable('assetFolders', table => { table.increments('id').primary() table.string('name').notNullable() table.string('slug').notNullable() table.integer('parentId').unsigned().references('id').inTable('assetFolders') }) // AUTHENTICATION ---------------------- .createTable('authentication', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() table.boolean('selfRegistration').notNullable().defaultTo(false) table.json('domainWhitelist').notNullable() table.json('autoEnrollGroups').notNullable() }) // COMMENTS ---------------------------- .createTable('comments', table => { table.increments('id').primary() table.text('content').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.integer('pageId').unsigned().references('id').inTable('pages') table.integer('authorId').unsigned().references('id').inTable('users') }) // EDITORS ----------------------------- .createTable('editors', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // GROUPS ------------------------------ .createTable('groups', table => { table.increments('id').primary() table.string('name').notNullable() table.json('permissions').notNullable() table.json('pageRules').notNullable() table.boolean('isSystem').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOCALES ----------------------------- .createTable('locales', table => { table.string('code', 5).notNullable().primary() table.json('strings') table.boolean('isRTL').notNullable().defaultTo(false) table.string('name').notNullable() table.string('nativeName').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOGGING ---------------------------- .createTable('loggers', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('level').notNullable().defaultTo('warn') table.json('config') }) // NAVIGATION ---------------------------- .createTable('navigation', table => { table.string('key').notNullable().primary() table.json('config') }) // PAGE HISTORY ------------------------ .createTable('pageHistory', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.integer('pageId').unsigned().references('id').inTable('pages') table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) // PAGES ------------------------------- .createTable('pages', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('privateNS') table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.text('render') table.json('toc') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') table.integer('creatorId').unsigned().references('id').inTable('users') }) // PAGE TREE --------------------------- .createTable('pageTree', table => { table.increments('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') table.integer('parent').unsigned().references('id').inTable('pageTree') table.integer('pageId').unsigned().references('id').inTable('pages') table.string('localeCode', 5).references('code').inTable('locales') }) // RENDERERS --------------------------- .createTable('renderers', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SEARCH ------------------------------ .createTable('searchEngines', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SETTINGS ---------------------------- .createTable('settings', table => { table.string('key').notNullable().primary() table.json('value') table.string('updatedAt').notNullable() }) // STORAGE ----------------------------- .createTable('storage', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push') table.json('config') }) // TAGS -------------------------------- .createTable('tags', table => { table.increments('id').primary() table.string('tag').notNullable().unique() table.string('title') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // USER KEYS --------------------------- .createTable('userKeys', table => { table.increments('id').primary() table.string('kind').notNullable() table.string('token').notNullable() table.string('createdAt').notNullable() table.string('validUntil').notNullable() table.integer('userId').unsigned().references('id').inTable('users') }) // USERS ------------------------------- .createTable('users', table => { table.increments('id').primary() table.string('email').notNullable() table.string('name').notNullable() table.string('providerId') table.string('password') table.boolean('tfaIsActive').notNullable().defaultTo(false) table.string('tfaSecret') table.string('jobTitle').defaultTo('') table.string('location').defaultTo('') table.string('pictureUrl') table.string('timezone').notNullable().defaultTo('America/New_York') table.boolean('isSystem').notNullable().defaultTo(false) table.boolean('isActive').notNullable().defaultTo(false) table.boolean('isVerified').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en') table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown') }) // ===================================== // RELATION TABLES // ===================================== // PAGE HISTORY TAGS --------------------------- .createTable('pageHistoryTags', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // PAGE TAGS --------------------------- .createTable('pageTags', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // USER GROUPS ------------------------- .createTable('userGroups', table => { table.increments('id').primary() table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE') table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE') }) // ===================================== // REFERENCES // ===================================== .table('users', table => { table.unique(['providerKey', 'email']) }) } exports.down = knex => { return knex.schema .dropTableIfExists('userGroups') .dropTableIfExists('pageHistoryTags') .dropTableIfExists('pageHistory') .dropTableIfExists('pageTags') .dropTableIfExists('assets') .dropTableIfExists('assetFolders') .dropTableIfExists('comments') .dropTableIfExists('editors') .dropTableIfExists('groups') .dropTableIfExists('locales') .dropTableIfExists('navigation') .dropTableIfExists('pages') .dropTableIfExists('renderers') .dropTableIfExists('settings') .dropTableIfExists('storage') .dropTableIfExists('tags') .dropTableIfExists('userKeys') .dropTableIfExists('users') } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.11.js ================================================ exports.up = knex => { return knex.schema .renameTable('pageHistory', 'pageHistory_old') .createTable('pageHistory', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('action').defaultTo('updated') table.integer('pageId').unsigned() table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) .raw(`INSERT INTO pageHistory SELECT id,path,hash,title,description,isPrivate,isPublished,publishStartDate,publishEndDate,content,contentType,createdAt,'updated' AS action,pageId,editorKey,localeCode,authorId FROM pageHistory_old;`) .dropTable('pageHistory_old') } exports.down = knex => { return knex.schema .renameTable('pageHistory', 'pageHistory_old') .createTable('pageHistory', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.integer('pageId').unsigned().references('id').inTable('pages') table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) .raw('INSERT INTO pageHistory SELECT id,path,hash,title,description,isPrivate,isPublished,publishStartDate,publishEndDate,content,contentType,createdAt,NULL as pageId,editorKey,localeCode,authorId FROM pageHistory_old;') .dropTable('pageHistory_old') } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.127.js ================================================ exports.up = knex => { return knex.schema .table('assets', table => { table.dropColumn('basename') table.string('hash').notNullable().defaultTo('') }) } exports.down = knex => { return knex.schema .table('assets', table => { table.dropColumn('hash') table.string('basename').notNullable().defaultTo('') }) } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.205.js ================================================ exports.up = knex => { return knex.schema .createTable('analytics', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) } exports.down = knex => { return knex.schema .dropTableIfExists('analytics') } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.217.js ================================================ exports.up = knex => { return knex.schema .table('locales', table => { table.integer('availability').notNullable().defaultTo(0) }) } exports.down = knex => { return knex.schema .table('locales', table => { table.dropColumn('availability') }) } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.242.js ================================================ exports.up = knex => { return knex.schema .table('users', table => { table.boolean('mustChangePwd').notNullable().defaultTo(false) }) } exports.down = knex => { return knex.schema .table('users', table => { table.dropColumn('mustChangePwd') }) } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.293.js ================================================ exports.up = knex => { return knex.schema .createTable('pageLinks', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('path').notNullable() table.string('localeCode', 5).notNullable() }) .table('pageLinks', table => { table.index(['path', 'localeCode']) }) } exports.down = knex => { return knex.schema .dropTableIfExists('pageLinks') } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.38.js ================================================ exports.up = knex => { return knex.schema .table('storage', table => { table.string('syncInterval') table.json('state') }) } exports.down = knex => { return knex.schema .table('storage', table => { table.dropColumn('syncInterval') table.dropColumn('state') }) } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-beta.99.js ================================================ exports.up = knex => { return knex.schema .createTable('assetData', table => { table.integer('id').primary() table.binary('data').notNullable() }) } exports.down = knex => { return knex.schema .dropTableIfExists('assetData') } ================================================ FILE: server/db/beta/migrations-sqlite/2.0.0-rc.2.js ================================================ exports.up = knex => { return knex.schema .dropTable('pageTree') .createTable('pageTree', table => { table.integer('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE') table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('localeCode', 5).references('code').inTable('locales') }) } exports.down = knex => { return knex.schema .dropTable('pageTree') .createTable('pageTree', table => { table.integer('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') table.integer('parent').unsigned().references('id').inTable('pageTree') table.integer('pageId').unsigned().references('id').inTable('pages') table.string('localeCode', 5).references('code').inTable('locales') }) } ================================================ FILE: server/db/migrations/2.0.0.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), selfCascadeDelete: WIKI.config.db.type !== 'mssql' } return knex.schema // ===================================== // MODEL TABLES // ===================================== // ANALYTICS --------------------------- .createTable('analytics', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // ASSETS ------------------------------ .createTable('assets', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('filename').notNullable() table.string('hash').notNullable() table.string('ext').notNullable() table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary') table.string('mime').notNullable().defaultTo('application/octet-stream') table.integer('fileSize').unsigned().comment('In kilobytes') table.json('metadata') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // ASSET DATA -------------------------- .createTable('assetData', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').primary() if (dbCompat.blobLength) { table.specificType('data', 'LONGBLOB').notNullable() } else { table.binary('data').notNullable() } }) // ASSET FOLDERS ----------------------- .createTable('assetFolders', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('name').notNullable() table.string('slug').notNullable() table.integer('parentId').unsigned().references('id').inTable('assetFolders') }) // AUTHENTICATION ---------------------- .createTable('authentication', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() table.boolean('selfRegistration').notNullable().defaultTo(false) table.json('domainWhitelist').notNullable() table.json('autoEnrollGroups').notNullable() }) // COMMENTS ---------------------------- .createTable('comments', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.text('content').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // EDITORS ----------------------------- .createTable('editors', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // GROUPS ------------------------------ .createTable('groups', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('name').notNullable() table.json('permissions').notNullable() table.json('pageRules').notNullable() table.boolean('isSystem').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOCALES ----------------------------- .createTable('locales', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('code', 5).notNullable().primary() table.json('strings') table.boolean('isRTL').notNullable().defaultTo(false) table.string('name').notNullable() table.string('nativeName').notNullable() table.integer('availability').notNullable().defaultTo(0) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOGGING ---------------------------- .createTable('loggers', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('level').notNullable().defaultTo('warn') table.json('config') }) // NAVIGATION ---------------------------- .createTable('navigation', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.json('config') }) // PAGE HISTORY ------------------------ .createTable('pageHistory', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.string('action').defaultTo('updated') table.integer('pageId').unsigned() table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() }) // PAGE LINKS -------------------------- .createTable('pageLinks', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.string('localeCode', 5).notNullable() }) // PAGES ------------------------------- .createTable('pages', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('privateNS') table.string('publishStartDate') table.string('publishEndDate') switch (WIKI.config.db.type) { case 'postgres': case 'sqlite': table.text('content') table.text('render') break case 'mariadb': case 'mysql': table.specificType('content', 'LONGTEXT') table.specificType('render', 'LONGTEXT') break case 'mssql': table.specificType('content', 'VARCHAR(max)') table.specificType('render', 'VARCHAR(max)') break } table.json('toc') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // PAGE TREE --------------------------- .createTable('pageTree', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').unsigned().primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') }) // RENDERERS --------------------------- .createTable('renderers', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SEARCH ------------------------------ .createTable('searchEngines', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SETTINGS ---------------------------- .createTable('settings', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.json('value') table.string('updatedAt').notNullable() }) // STORAGE ----------------------------- .createTable('storage', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push') table.json('config') table.string('syncInterval') table.json('state') }) // TAGS -------------------------------- .createTable('tags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('tag').notNullable().unique() table.string('title') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // USER KEYS --------------------------- .createTable('userKeys', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('kind').notNullable() table.string('token').notNullable() table.string('createdAt').notNullable() table.string('validUntil').notNullable() }) // USERS ------------------------------- .createTable('users', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('email').notNullable() table.string('name').notNullable() table.string('providerId') table.string('password') table.boolean('tfaIsActive').notNullable().defaultTo(false) table.string('tfaSecret') table.string('jobTitle').defaultTo('') table.string('location').defaultTo('') table.string('pictureUrl') table.string('timezone').notNullable().defaultTo('America/New_York') table.boolean('isSystem').notNullable().defaultTo(false) table.boolean('isActive').notNullable().defaultTo(false) table.boolean('isVerified').notNullable().defaultTo(false) table.boolean('mustChangePwd').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // ===================================== // RELATION TABLES // ===================================== // PAGE HISTORY TAGS --------------------------- .createTable('pageHistoryTags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // PAGE TAGS --------------------------- .createTable('pageTags', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // USER GROUPS ------------------------- .createTable('userGroups', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE') table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE') }) // ===================================== // REFERENCES // ===================================== .table('assets', table => { table.integer('folderId').unsigned().references('id').inTable('assetFolders') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('comments', table => { table.integer('pageId').unsigned().references('id').inTable('pages') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('pageHistory', table => { table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) .table('pageLinks', table => { table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.index(['path', 'localeCode']) }) .table('pages', table => { table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') table.integer('creatorId').unsigned().references('id').inTable('users') }) .table('pageTree', table => { if (dbCompat.selfCascadeDelete) { table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE') } else { table.integer('parent').unsigned() } table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('localeCode', 5).references('code').inTable('locales') }) .table('userKeys', table => { table.integer('userId').unsigned().references('id').inTable('users') }) .table('users', table => { table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en') table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown') table.unique(['providerKey', 'email']) }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.1.85.js ================================================ /* global WIKI */ exports.up = knex => { return knex.schema .alterTable('pageHistory', table => { switch (WIKI.config.db.type) { // No change needed for PostgreSQL and SQLite case 'mariadb': case 'mysql': table.specificType('content', 'LONGTEXT').alter() break case 'mssql': table.specificType('content', 'VARCHAR(max)').alter() break } }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.2.17.js ================================================ const _ = require('lodash') /* global WIKI */ exports.up = async knex => { let sqlVersionDate = '' switch (WIKI.config.db.type) { case 'postgres': sqlVersionDate = 'UPDATE "pageHistory" h1 SET "versionDate" = COALESCE((SELECT prev."createdAt" FROM "pageHistory" prev WHERE prev."pageId" = h1."pageId" AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1), h1."createdAt")' break case 'mssql': sqlVersionDate = 'UPDATE h1 SET "versionDate" = COALESCE((SELECT TOP 1 prev."createdAt" FROM "pageHistory" prev WHERE prev."pageId" = h1."pageId" AND prev.id < h1.id ORDER BY prev.id DESC), h1."createdAt") FROM "pageHistory" h1' break case 'mysql': case 'mariadb': // -> Fix for 2.2.50 failed migration const pageHistoryColumns = await knex.schema.raw('SHOW COLUMNS FROM pageHistory') if (_.some(pageHistoryColumns[0], ['Field', 'versionDate'])) { console.info('MySQL 2.2.50 Migration Fix - Dropping failed versionDate column...') await knex.schema.raw('ALTER TABLE pageHistory DROP COLUMN versionDate') console.info('versionDate column dropped successfully.') } sqlVersionDate = `UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM (SELECT * FROM pageHistory) AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt` break // case 'mariadb': // sqlVersionDate = `UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM pageHistory AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt` // break } await knex.schema .alterTable('pageHistory', table => { table.string('versionDate').notNullable().defaultTo('') }) .raw(sqlVersionDate) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.2.3.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('apiKeys', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.increments('id').primary() table.string('name').notNullable() table.text('key').notNullable() table.string('expiration').notNullable() table.boolean('isRevoked').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.3.10.js ================================================ exports.up = knex => { return knex.schema .alterTable('users', table => { table.string('lastLoginAt') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.3.23.js ================================================ exports.up = knex => { return knex.schema .alterTable('pageTree', table => { table.json('ancestors') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.4.13.js ================================================ /* global WIKI */ exports.up = knex => { return knex.schema .alterTable('pages', table => { if (WIKI.config.db.type === 'mysql') { table.json('extra') } else { table.json('extra').notNullable().defaultTo('{}') } }) .alterTable('pageHistory', table => { if (WIKI.config.db.type === 'mysql') { table.json('extra') } else { table.json('extra').notNullable().defaultTo('{}') } }) .alterTable('users', table => { table.string('dateFormat').notNullable().defaultTo('') table.string('appearance').notNullable().defaultTo('') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.4.14.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('commentProviders', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.4.36.js ================================================ exports.up = knex => { return knex.schema .alterTable('comments', table => { table.text('render').notNullable().defaultTo('') table.string('name').notNullable().defaultTo('') table.string('email').notNullable().defaultTo('') table.string('ip').notNullable().defaultTo('') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.4.61.js ================================================ exports.up = knex => { return knex.schema .alterTable('comments', table => { table.integer('replyTo').unsigned().notNullable().defaultTo(0) }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.1.js ================================================ exports.up = async knex => { // Check for users using disabled strategies let protectedStrategies = [] const disabledStrategies = await knex('authentication').where('isEnabled', false) if (disabledStrategies) { const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key)) if (incompatibleUsers && incompatibleUsers.length > 0) { protectedStrategies = incompatibleUsers.map(u => u.providerKey) } } // Delete disabled strategies await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del() // Update table schema await knex.schema .alterTable('authentication', table => { table.integer('order').unsigned().notNullable().defaultTo(0) table.string('strategyKey').notNullable().defaultTo('') table.string('displayName').notNullable().defaultTo('') }) // Fix pre-2.5 strategies const strategies = await knex('authentication') let idx = 1 for (const strategy of strategies) { await knex('authentication').where('key', strategy.key).update({ strategyKey: strategy.key, order: (strategy.key === 'local') ? 0 : idx++ }) } } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.108.js ================================================ const has = require('lodash/has') exports.up = async knex => { // -> Fix 2.5.1 added isEnabled columns for beta users const localStrategy = await knex('authentication').where('key', 'local').first() if (localStrategy && !has(localStrategy, 'isEnabled')) { await knex.schema .alterTable('authentication', table => { table.boolean('isEnabled').notNullable().defaultTo(true) }) } } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.118.js ================================================ exports.up = async knex => { // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382) await knex('authentication').where('key', 'local').update({ isEnabled: true }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.12.js ================================================ exports.up = async knex => { await knex.schema .alterTable('groups', table => { table.string('redirectOnLogin').notNullable().defaultTo('/') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.122.js ================================================ /* global WIKI */ exports.up = knex => { const dbCompat = { blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`), charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`) } return knex.schema .createTable('userAvatars', table => { if (dbCompat.charset) { table.charset('utf8mb4') } table.integer('id').primary() if (dbCompat.blobLength) { table.specificType('data', 'LONGBLOB').notNullable() } else { table.binary('data').notNullable() } }) } exports.down = knex => { } ================================================ FILE: server/db/migrations/2.5.128.js ================================================ exports.up = async knex => { await knex('users').update({ email: knex.raw('LOWER(??)', ['email']) }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.0.0.js ================================================ exports.up = knex => { return knex.schema // ===================================== // MODEL TABLES // ===================================== // ANALYTICS --------------------------- .createTable('analytics', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // ASSETS ------------------------------ .createTable('assets', table => { table.increments('id').primary() table.string('filename').notNullable() table.string('hash').notNullable().defaultTo('') table.string('ext').notNullable() table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary') table.string('mime').notNullable().defaultTo('application/octet-stream') table.integer('fileSize').unsigned().comment('In kilobytes') table.json('metadata') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.integer('folderId').unsigned().references('id').inTable('assetFolders') table.integer('authorId').unsigned().references('id').inTable('users') }) // ASSET DATA -------------------------- .createTable('assetData', table => { table.integer('id').primary() table.binary('data').notNullable() }) // ASSET FOLDERS ----------------------- .createTable('assetFolders', table => { table.increments('id').primary() table.string('name').notNullable() table.string('slug').notNullable() table.integer('parentId').unsigned().references('id').inTable('assetFolders') }) // AUTHENTICATION ---------------------- .createTable('authentication', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() table.boolean('selfRegistration').notNullable().defaultTo(false) table.json('domainWhitelist').notNullable() table.json('autoEnrollGroups').notNullable() }) // COMMENTS ---------------------------- .createTable('comments', table => { table.increments('id').primary() table.text('content').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.integer('pageId').unsigned().references('id').inTable('pages') table.integer('authorId').unsigned().references('id').inTable('users') }) // EDITORS ----------------------------- .createTable('editors', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) // GROUPS ------------------------------ .createTable('groups', table => { table.increments('id').primary() table.string('name').notNullable() table.json('permissions').notNullable() table.json('pageRules').notNullable() table.boolean('isSystem').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOCALES ----------------------------- .createTable('locales', table => { table.string('code', 5).notNullable().primary() table.json('strings') table.boolean('isRTL').notNullable().defaultTo(false) table.string('name').notNullable() table.string('nativeName').notNullable() table.integer('availability').notNullable().defaultTo(0) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // LOGGING ---------------------------- .createTable('loggers', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('level').notNullable().defaultTo('warn') table.json('config') }) // NAVIGATION ---------------------------- .createTable('navigation', table => { table.string('key').notNullable().primary() table.json('config') }) // PAGE HISTORY ------------------------ .createTable('pageHistory', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('action').defaultTo('updated') table.integer('pageId').unsigned() table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') }) // PAGE LINKS -------------------------- .createTable('pageLinks', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('path').notNullable() table.string('localeCode', 5).notNullable() }) // PAGES ------------------------------- .createTable('pages', table => { table.increments('id').primary() table.string('path').notNullable() table.string('hash').notNullable() table.string('title').notNullable() table.string('description') table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isPublished').notNullable().defaultTo(false) table.string('privateNS') table.string('publishStartDate') table.string('publishEndDate') table.text('content') table.text('render') table.json('toc') table.string('contentType').notNullable() table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 5).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') table.integer('creatorId').unsigned().references('id').inTable('users') }) // PAGE TREE --------------------------- .createTable('pageTree', table => { table.integer('id').primary() table.string('path').notNullable() table.integer('depth').unsigned().notNullable() table.string('title').notNullable() table.boolean('isPrivate').notNullable().defaultTo(false) table.boolean('isFolder').notNullable().defaultTo(false) table.string('privateNS') table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE') table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.string('localeCode', 5).references('code').inTable('locales') }) // RENDERERS --------------------------- .createTable('renderers', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SEARCH ------------------------------ .createTable('searchEngines', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config') }) // SETTINGS ---------------------------- .createTable('settings', table => { table.string('key').notNullable().primary() table.json('value') table.string('updatedAt').notNullable() }) // STORAGE ----------------------------- .createTable('storage', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push') table.json('config') table.string('syncInterval') table.json('state') }) // TAGS -------------------------------- .createTable('tags', table => { table.increments('id').primary() table.string('tag').notNullable().unique() table.string('title') table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) // USER KEYS --------------------------- .createTable('userKeys', table => { table.increments('id').primary() table.string('kind').notNullable() table.string('token').notNullable() table.string('createdAt').notNullable() table.string('validUntil').notNullable() table.integer('userId').unsigned().references('id').inTable('users') }) // USERS ------------------------------- .createTable('users', table => { table.increments('id').primary() table.string('email').notNullable() table.string('name').notNullable() table.string('providerId') table.string('password') table.boolean('tfaIsActive').notNullable().defaultTo(false) table.string('tfaSecret') table.string('jobTitle').defaultTo('') table.string('location').defaultTo('') table.string('pictureUrl') table.string('timezone').notNullable().defaultTo('America/New_York') table.boolean('isSystem').notNullable().defaultTo(false) table.boolean('isActive').notNullable().defaultTo(false) table.boolean('isVerified').notNullable().defaultTo(false) table.boolean('mustChangePwd').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en') table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown') }) // ===================================== // RELATION TABLES // ===================================== // PAGE HISTORY TAGS --------------------------- .createTable('pageHistoryTags', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // PAGE TAGS --------------------------- .createTable('pageTags', table => { table.increments('id').primary() table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE') table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') }) // USER GROUPS ------------------------- .createTable('userGroups', table => { table.increments('id').primary() table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE') table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE') }) // ===================================== // REFERENCES // ===================================== .table('users', table => { table.unique(['providerKey', 'email']) }) // ===================================== // INDEXES // ===================================== .table('pageLinks', table => { table.index(['path', 'localeCode']) }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.2.17.js ================================================ exports.up = knex => { return knex.schema .alterTable('pageHistory', table => { table.string('versionDate').notNullable().defaultTo('') }) .raw(`UPDATE pageHistory AS h1 SET versionDate = COALESCE((SELECT createdAt FROM pageHistory AS h2 WHERE h2.pageId = h1.pageId AND h2.id < h1.id ORDER BY h2.id DESC LIMIT 1), h1.createdAt, '')`) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.2.3.js ================================================ exports.up = knex => { return knex.schema .createTable('apiKeys', table => { table.increments('id').primary() table.string('name').notNullable() table.text('key').notNullable() table.string('expiration').notNullable() table.boolean('isRevoked').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.3.10.js ================================================ exports.up = knex => { return knex.schema .alterTable('users', table => { table.string('lastLoginAt') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.3.14.js ================================================ exports.up = knex => { return knex.schema .createTable('commentProviders', table => { table.string('key').notNullable().primary() table.boolean('isEnabled').notNullable().defaultTo(false) table.json('config').notNullable() }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.3.23.js ================================================ exports.up = knex => { return knex.schema .alterTable('pageTree', table => { table.json('ancestors') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.4.13.js ================================================ exports.up = knex => { return knex.schema .alterTable('pages', table => { table.json('extra').notNullable().defaultTo('{}') }) .alterTable('pageHistory', table => { table.json('extra').notNullable().defaultTo('{}') }) .alterTable('users', table => { table.string('dateFormat').notNullable().defaultTo('') table.string('appearance').notNullable().defaultTo('') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.4.36.js ================================================ exports.up = knex => { return knex.schema .alterTable('comments', table => { table.text('render').notNullable().defaultTo('') table.string('name').notNullable().defaultTo('') table.string('email').notNullable().defaultTo('') table.string('ip').notNullable().defaultTo('') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.4.61.js ================================================ exports.up = knex => { return knex.schema .alterTable('comments', table => { table.integer('replyTo').unsigned().notNullable().defaultTo(0) }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.1.js ================================================ exports.up = async knex => { // Check for users using disabled strategies let protectedStrategies = [] const disabledStrategies = await knex('authentication').where('isEnabled', false) if (disabledStrategies) { const incompatibleUsers = await knex('users').distinct('providerKey').whereIn('providerKey', disabledStrategies.map(s => s.key)) if (incompatibleUsers && incompatibleUsers.length > 0) { protectedStrategies = incompatibleUsers.map(u => u.providerKey) } } // Delete disabled strategies await knex('authentication').whereNotIn('key', protectedStrategies).andWhere('isEnabled', false).del() // Update table schema await knex.schema .alterTable('authentication', table => { table.integer('order').unsigned().notNullable().defaultTo(0) table.string('strategyKey').notNullable().defaultTo('') table.string('displayName').notNullable().defaultTo('') }) // Fix pre-2.5 strategies const strategies = await knex('authentication') let idx = 1 for (const strategy of strategies) { await knex('authentication').where('key', strategy.key).update({ strategyKey: strategy.key, order: (strategy.key === 'local') ? 0 : idx++ }) } } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.108.js ================================================ const has = require('lodash/has') exports.up = async knex => { // -> Fix 2.5.1 added isEnabled columns for beta users const localStrategy = await knex('authentication').where('key', 'local').first() if (localStrategy && !has(localStrategy, 'isEnabled')) { await knex.schema .alterTable('authentication', table => { table.boolean('isEnabled').notNullable().defaultTo(true) }) } } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.118.js ================================================ exports.up = async knex => { // -> Fix 2.5.117 new installations without isEnabled on local auth (#2382) await knex('authentication').where('key', 'local').update({ isEnabled: true }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.12.js ================================================ exports.up = async knex => { await knex.schema .alterTable('groups', table => { table.string('redirectOnLogin').notNullable().defaultTo('/') }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.122.js ================================================ exports.up = knex => { return knex.schema .createTable('userAvatars', table => { table.integer('id').primary() table.binary('data').notNullable() }) } exports.down = knex => { } ================================================ FILE: server/db/migrations-sqlite/2.5.128.js ================================================ exports.up = async knex => { await knex('users').update({ email: knex.raw('LOWER(email)') }) } exports.down = knex => { } ================================================ FILE: server/db/migrator-source.js ================================================ const path = require('path') const fs = require('fs-extra') const semver = require('semver') const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/migrations' : 'db/migrations-sqlite') /* global WIKI */ module.exports = { /** * Gets the migration names * @returns Promise */ async getMigrations() { const migrationFiles = await fs.readdir(baseMigrationPath) return migrationFiles.map(m => m.replace('.js', '')).sort(semver.compare).map(m => ({ file: m, directory: baseMigrationPath })) }, getMigrationName(migration) { return migration.file.indexOf('.js') >= 0 ? migration.file : `${migration.file}.js` }, getMigration(migration) { return require(path.join(baseMigrationPath, migration.file)) } } ================================================ FILE: server/graph/directives/auth.js ================================================ const { SchemaDirectiveVisitor } = require('graphql-tools') const { defaultFieldResolver } = require('graphql') const _ = require('lodash') class AuthDirective extends SchemaDirectiveVisitor { visitObject(type) { this.ensureFieldsWrapped(type) type._requiredAuthScopes = this.args.requires } // Visitor methods for nested types like fields and arguments // also receive a details object that provides information about // the parent and grandparent types. visitFieldDefinition(field, details) { this.ensureFieldsWrapped(details.objectType) field._requiredAuthScopes = this.args.requires } visitArgumentDefinition(argument, details) { this.ensureFieldsWrapped(details.objectType) argument._requiredAuthScopes = this.args.requires } ensureFieldsWrapped(objectType) { // Mark the GraphQLObjectType object to avoid re-wrapping: if (objectType._authFieldsWrapped) return objectType._authFieldsWrapped = true const fields = objectType.getFields() Object.keys(fields).forEach(fieldName => { const field = fields[fieldName] const { resolve = defaultFieldResolver } = field field.resolve = async function (...args) { // Get the required scopes from the field first, falling back // to the objectType if no scopes is required by the field: const requiredScopes = field._requiredAuthScopes || objectType._requiredAuthScopes if (!requiredScopes) { return resolve.apply(this, args) } const context = args[2] if (!context.req.user) { throw new Error('Unauthorized') } if (!_.some(context.req.user.permissions, pm => _.includes(requiredScopes, pm))) { throw new Error('Forbidden') } return resolve.apply(this, args) } }) } } module.exports = AuthDirective ================================================ FILE: server/graph/directives/rate-limit.js ================================================ const { createRateLimitDirective } = require('graphql-rate-limit-directive') module.exports = createRateLimitDirective({ keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${info.parentType}.${info.fieldName}` }) ================================================ FILE: server/graph/index.js ================================================ const _ = require('lodash') const fs = require('fs') // const gqlTools = require('graphql-tools') const path = require('path') const autoload = require('auto-load') const PubSub = require('graphql-subscriptions').PubSub const { LEVEL, MESSAGE } = require('triple-beam') const Transport = require('winston-transport') const { createRateLimitTypeDef } = require('graphql-rate-limit-directive') // const { GraphQLUpload } = require('graphql-upload') /* global WIKI */ WIKI.logger.info(`Loading GraphQL Schema...`) // Init Subscription PubSub WIKI.GQLEmitter = new PubSub() // Schemas let typeDefs = [createRateLimitTypeDef()] let schemas = fs.readdirSync(path.join(WIKI.SERVERPATH, 'graph/schemas')) schemas.forEach(schema => { typeDefs.push(fs.readFileSync(path.join(WIKI.SERVERPATH, `graph/schemas/${schema}`), 'utf8')) }) // Resolvers let resolvers = { // Upload: GraphQLUpload } const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers'))) resolversObj.forEach(resolver => { _.merge(resolvers, resolver) }) // Directives let schemaDirectives = { ...autoload(path.join(WIKI.SERVERPATH, 'graph/directives')) } // Live Trail Logger (admin) class LiveTrailLogger extends Transport { constructor(opts) { super(opts) this.name = 'liveTrailLogger' this.level = 'debug' } log (info, callback = () => {}) { WIKI.GQLEmitter.publish('livetrail', { loggingLiveTrail: { timestamp: new Date(), level: info[LEVEL], output: info[MESSAGE] } }) callback(null, true) } } WIKI.logger.add(new LiveTrailLogger({})) WIKI.logger.info(`GraphQL Schema: [ OK ]`) module.exports = { typeDefs, resolvers, schemaDirectives } ================================================ FILE: server/graph/resolvers/analytics.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async analytics() { return {} } }, Mutation: { async analytics() { return {} } }, AnalyticsQuery: { async providers(obj, args, context, info) { let providers = await WIKI.models.analytics.getProviders(args.isEnabled) providers = providers.map(stg => { const providerInfo = _.find(WIKI.data.analytics, ['key', stg.key]) || {} return { ...providerInfo, ...stg, config: _.sortBy(_.transform(stg.config, (res, value, key) => { const configData = _.get(providerInfo.props, key, {}) res.push({ key, value: JSON.stringify({ ...configData, value }) }) }, []), 'key') } }) return providers } }, AnalyticsMutation: { async updateProviders(obj, args, context) { try { for (let str of args.providers) { await WIKI.models.analytics.query().patch({ isEnabled: str.isEnabled, config: _.reduce(str.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) return result }, {}) }).where('key', str.key) await WIKI.cache.del('analytics') } return { responseResult: graphHelper.generateSuccess('Providers updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/asset.js ================================================ const _ = require('lodash') const sanitize = require('sanitize-filename') const graphHelper = require('../../helpers/graph') const assetHelper = require('../../helpers/asset') /* global WIKI */ module.exports = { Query: { async assets() { return {} } }, Mutation: { async assets() { return {} } }, AssetQuery: { async list(obj, args, context) { let cond = { folderId: args.folderId === 0 ? null : args.folderId } if (args.kind !== 'ALL') { cond.kind = args.kind.toLowerCase() } const folderHierarchy = await WIKI.models.assetFolders.getHierarchy(args.folderId) const folderPath = folderHierarchy.map(h => h.slug).join('/') const results = await WIKI.models.assets.query().where(cond) return _.filter(results, r => { const path = folderPath ? `${folderPath}/${r.filename}` : r.filename return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path }) }).map(a => ({ ...a, kind: a.kind.toUpperCase() })) }, async folders(obj, args, context) { const results = await WIKI.models.assetFolders.query().where({ parentId: args.parentFolderId === 0 ? null : args.parentFolderId }) const parentHierarchy = await WIKI.models.assetFolders.getHierarchy(args.parentFolderId) const parentPath = parentHierarchy.map(h => h.slug).join('/') return _.filter(results, r => { const path = parentPath ? `${parentPath}/${r.slug}` : r.slug return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path }) }) } }, AssetMutation: { /** * Create New Asset Folder */ async createFolder(obj, args, context) { try { const folderSlug = sanitize(args.slug).toLowerCase() const parentFolderId = args.parentFolderId === 0 ? null : args.parentFolderId const result = await WIKI.models.assetFolders.query().where({ parentId: parentFolderId, slug: folderSlug }).first() if (!result) { await WIKI.models.assetFolders.query().insert({ slug: folderSlug, name: folderSlug, parentId: parentFolderId }) return { responseResult: graphHelper.generateSuccess('Asset Folder has been created successfully.') } } else { throw new WIKI.Error.AssetFolderExists() } } catch (err) { return graphHelper.generateError(err) } }, /** * Rename an Asset */ async renameAsset(obj, args, context) { try { const filename = sanitize(args.filename).toLowerCase() const asset = await WIKI.models.assets.query().findById(args.id) if (asset) { // Check for extension mismatch if (!_.endsWith(filename, asset.ext)) { throw new WIKI.Error.AssetRenameInvalidExt() } // Check for non-dot files changing to dotfile if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) { throw new WIKI.Error.AssetRenameInvalid() } // Check for collision const assetCollision = await WIKI.models.assets.query().where({ filename, folderId: asset.folderId }).first() if (assetCollision) { throw new WIKI.Error.AssetRenameCollision() } // Get asset folder path let hierarchy = [] if (asset.folderId) { hierarchy = await WIKI.models.assetFolders.getHierarchy(asset.folderId) } // Check source asset permissions const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) { throw new WIKI.Error.AssetRenameForbidden() } // Check target asset permissions const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) { throw new WIKI.Error.AssetRenameTargetForbidden() } // Update filename + hash const fileHash = assetHelper.generateHash(assetTargetPath) await WIKI.models.assets.query().patch({ filename: filename, hash: fileHash }).findById(args.id) // Delete old asset cache await asset.deleteAssetCache() // Rename in Storage await WIKI.models.storage.assetEvent({ event: 'renamed', asset: { ...asset, path: assetSourcePath, destinationPath: assetTargetPath, moveAuthorId: context.req.user.id, moveAuthorName: context.req.user.name, moveAuthorEmail: context.req.user.email } }) return { responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.') } } else { throw new WIKI.Error.AssetInvalid() } } catch (err) { return graphHelper.generateError(err) } }, /** * Delete an Asset */ async deleteAsset(obj, args, context) { try { const asset = await WIKI.models.assets.query().findById(args.id) if (asset) { // Check permissions const assetPath = await asset.getAssetPath() if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) { throw new WIKI.Error.AssetDeleteForbidden() } await WIKI.models.knex('assetData').where('id', args.id).del() await WIKI.models.assets.query().deleteById(args.id) await asset.deleteAssetCache() // Delete from Storage await WIKI.models.storage.assetEvent({ event: 'deleted', asset: { ...asset, path: assetPath, authorId: context.req.user.id, authorName: context.req.user.name, authorEmail: context.req.user.email } }) return { responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.') } } else { throw new WIKI.Error.AssetInvalid() } } catch (err) { return graphHelper.generateError(err) } }, /** * Flush Temporary Uploads */ async flushTempUploads(obj, args, context) { try { await WIKI.models.assets.flushTempUploads() return { responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.') } } catch (err) { return graphHelper.generateError(err) } } } // File: { // folder(fl) { // return fl.getFolder() // } // } } ================================================ FILE: server/graph/resolvers/authentication.js ================================================ const _ = require('lodash') const fs = require('fs-extra') const path = require('path') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async authentication () { return {} } }, Mutation: { async authentication () { return {} } }, AuthenticationQuery: { /** * List of API Keys */ async apiKeys (obj, args, context) { const keys = await WIKI.models.apiKeys.query().orderBy(['isRevoked', 'name']) return keys.map(k => ({ id: k.id, name: k.name, keyShort: '...' + k.key.substring(k.key.length - 20), isRevoked: k.isRevoked, expiration: k.expiration, createdAt: k.createdAt, updatedAt: k.updatedAt })) }, /** * Current API State */ apiState () { return WIKI.config.api.isEnabled }, async strategies () { return WIKI.data.authentication.map(stg => ({ ...stg, isAvailable: stg.isAvailable === true, props: _.sortBy(_.transform(stg.props, (res, value, key) => { res.push({ key, value: JSON.stringify(value) }) }, []), 'key') })) }, /** * Fetch active authentication strategies */ async activeStrategies (obj, args, context, info) { let strategies = await WIKI.models.authentication.getStrategies() strategies = strategies.map(stg => { const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {} return { ...stg, strategy: strategyInfo, config: _.sortBy(_.transform(stg.config, (res, value, key) => { const configData = _.get(strategyInfo.props, key, false) if (configData) { res.push({ key, value: JSON.stringify({ ...configData, value }) }) } }, []), 'key') } }) return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies } }, AuthenticationMutation: { /** * Create New API Key */ async createApiKey (obj, args, context) { try { const key = await WIKI.models.apiKeys.createNewKey(args) await WIKI.auth.reloadApiKeys() WIKI.events.outbound.emit('reloadApiKeys') return { key, responseResult: graphHelper.generateSuccess('API Key created successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Perform Login */ async login (obj, args, context) { try { const authResult = await WIKI.models.users.login(args, context) return { ...authResult, responseResult: graphHelper.generateSuccess('Login success') } } catch (err) { // LDAP Debug Flag if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) { WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err) } return graphHelper.generateError(err) } }, /** * Perform 2FA Login */ async loginTFA (obj, args, context) { try { const authResult = await WIKI.models.users.loginTFA(args, context) return { ...authResult, responseResult: graphHelper.generateSuccess('TFA success') } } catch (err) { return graphHelper.generateError(err) } }, /** * Perform Mandatory Password Change after Login */ async loginChangePassword (obj, args, context) { try { const authResult = await WIKI.models.users.loginChangePassword(args, context) return { ...authResult, responseResult: graphHelper.generateSuccess('Password changed successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Perform Mandatory Password Change after Login */ async forgotPassword (obj, args, context) { try { await WIKI.models.users.loginForgotPassword(args, context) return { responseResult: graphHelper.generateSuccess('Password reset request processed.') } } catch (err) { return graphHelper.generateError(err) } }, /** * Register a new account */ async register (obj, args, context) { try { await WIKI.models.users.register({ ...args, verify: true }, context) return { responseResult: graphHelper.generateSuccess('Registration success') } } catch (err) { return graphHelper.generateError(err) } }, /** * Set API state */ async setApiState (obj, args, context) { try { WIKI.config.api.isEnabled = args.enabled await WIKI.configSvc.saveToDb(['api']) return { responseResult: graphHelper.generateSuccess('API State changed successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Revoke an API key */ async revokeApiKey (obj, args, context) { try { await WIKI.models.apiKeys.query().findById(args.id).patch({ isRevoked: true }) await WIKI.auth.reloadApiKeys() WIKI.events.outbound.emit('reloadApiKeys') return { responseResult: graphHelper.generateSuccess('API Key revoked successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Update Authentication Strategies */ async updateStrategies (obj, args, context) { try { const previousStrategies = await WIKI.models.authentication.getStrategies() for (const str of args.strategies) { const newStr = { displayName: str.displayName, order: str.order, isEnabled: str.isEnabled, config: _.reduce(str.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) return result }, {}), selfRegistration: str.selfRegistration, domainWhitelist: { v: str.domainWhitelist }, autoEnrollGroups: { v: str.autoEnrollGroups } } if (_.some(previousStrategies, ['key', str.key])) { await WIKI.models.authentication.query().patch({ key: str.key, strategyKey: str.strategyKey, ...newStr }).where('key', str.key) } else { await WIKI.models.authentication.query().insert({ key: str.key, strategyKey: str.strategyKey, ...newStr }) } } for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) { const hasUsers = await WIKI.models.users.query().count('* as total').where({ providerKey: str.key }).first() if (_.toSafeInteger(hasUsers.total) > 0) { throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`) } else { await WIKI.models.authentication.query().delete().where('key', str.key) } } await WIKI.auth.activateStrategies() WIKI.events.outbound.emit('reloadAuthStrategies') return { responseResult: graphHelper.generateSuccess('Strategies updated successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Generate New Authentication Public / Private Key Certificates */ async regenerateCertificates (obj, args, context) { try { await WIKI.auth.regenerateCertificates() return { responseResult: graphHelper.generateSuccess('Certificates have been regenerated successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * Reset Guest User */ async resetGuestUser (obj, args, context) { try { await WIKI.auth.resetGuestUser() return { responseResult: graphHelper.generateSuccess('Guest user has been reset successfully.') } } catch (err) { return graphHelper.generateError(err) } } }, AuthenticationStrategy: { icon (ap, args) { return fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${ap.key}.svg`), 'utf8').catch(err => { if (err.code === 'ENOENT') { return null } throw err }) } } } ================================================ FILE: server/graph/resolvers/comment.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async comments() { return {} } }, Mutation: { async comments() { return {} } }, CommentQuery: { /** * Fetch list of Comments Providers */ async providers(obj, args, context, info) { const providers = await WIKI.models.commentProviders.getProviders() return providers.map(provider => { const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {} return { ...providerInfo, ...provider, config: _.sortBy(_.transform(provider.config, (res, value, key) => { const configData = _.get(providerInfo.props, key, false) if (configData) { res.push({ key, value: JSON.stringify({ ...configData, value }) }) } }, []), 'key') } }) }, /** * Fetch list of comments for a page */ async list (obj, args, context) { const page = await WIKI.models.pages.query().select('pages.id').findOne({ localeCode: args.locale, path: args.path }) .withGraphJoined('tags') .modifyGraph('tags', builder => { builder.select('tag') }) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], { tags: page.tags, ...args })) { const comments = await WIKI.models.comments.query().where('pageId', page.id).orderBy('createdAt') return comments.map(c => ({ ...c, authorName: c.name, authorEmail: c.email, authorIP: c.ip })) } else { throw new WIKI.Error.CommentViewForbidden() } } else { return [] } }, /** * Fetch a single comment */ async single (obj, args, context) { const cm = await WIKI.data.commentProvider.getCommentById(args.id) if (!cm || !cm.pageId) { throw new WIKI.Error.CommentNotFound() } const page = await WIKI.models.pages.query().select('localeCode', 'path').findById(cm.pageId) .withGraphJoined('tags') .modifyGraph('tags', builder => { builder.select('tag') }) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], { path: page.path, locale: page.localeCode, tags: page.tags })) { return { ...cm, authorName: cm.name, authorEmail: cm.email, authorIP: cm.ip } } else { throw new WIKI.Error.CommentViewForbidden() } } else { WIKI.logger.warn(`Comment #${cm.id} is linked to a page #${cm.pageId} that doesn't exist! [ERROR]`) throw new WIKI.Error.CommentGenericError() } } }, CommentMutation: { /** * Create New Comment */ async create (obj, args, context) { try { const cmId = await WIKI.models.comments.postNewComment({ ...args, user: context.req.user, ip: context.req.ip }) return { responseResult: graphHelper.generateSuccess('New comment posted successfully'), id: cmId } } catch (err) { return graphHelper.generateError(err) } }, /** * Update an Existing Comment */ async update (obj, args, context) { try { const cmRender = await WIKI.models.comments.updateComment({ ...args, user: context.req.user, ip: context.req.ip }) return { responseResult: graphHelper.generateSuccess('Comment updated successfully'), render: cmRender } } catch (err) { return graphHelper.generateError(err) } }, /** * Delete an Existing Comment */ async delete (obj, args, context) { try { await WIKI.models.comments.deleteComment({ id: args.id, user: context.req.user, ip: context.req.ip }) return { responseResult: graphHelper.generateSuccess('Comment deleted successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * Update Comments Providers */ async updateProviders(obj, args, context) { try { for (let provider of args.providers) { await WIKI.models.commentProviders.query().patch({ isEnabled: provider.isEnabled, config: _.reduce(provider.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) return result }, {}) }).where('key', provider.key) } await WIKI.models.commentProviders.initProvider() return { responseResult: graphHelper.generateSuccess('Comment Providers updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/contribute.js ================================================ const request = require('request-promise') const _ = require('lodash') /* global WIKI */ module.exports = { Query: { async contribute() { return {} } }, ContributeQuery: { async contributors(obj, args, context, info) { try { const resp = await request({ method: 'POST', uri: 'https://graph.requarks.io', json: true, body: { query: '{\n sponsors {\n list(kind: BACKER) {\n id\n source\n name\n joined\n website\n twitter\n avatar\n }\n }\n}\n', variables: {} } }) return _.get(resp, 'data.sponsors.list', []) } catch (err) { WIKI.logger.warn(err) } } } } ================================================ FILE: server/graph/resolvers/folder.js ================================================ module.exports = { // Query: { // folders(obj, args, context, info) { // return WIKI.models.Folder.findAll({ where: args }) // } // }, // Mutation: { // createFolder(obj, args) { // return WIKI.models.Folder.create(args) // }, // deleteFolder(obj, args) { // return WIKI.models.Folder.destroy({ // where: { // id: args.id // }, // limit: 1 // }) // }, // renameFolder(obj, args) { // return WIKI.models.Folder.update({ // name: args.name // }, { // where: { id: args.id } // }) // } // }, // Folder: { // files(grp) { // return grp.getFiles() // } // } } ================================================ FILE: server/graph/resolvers/group.js ================================================ const graphHelper = require('../../helpers/graph') const safeRegex = require('safe-regex') const _ = require('lodash') const gql = require('graphql') /* global WIKI */ module.exports = { Query: { async groups () { return {} } }, Mutation: { async groups () { return {} } }, GroupQuery: { /** * FETCH ALL GROUPS */ async list () { return WIKI.models.groups.query().select( 'groups.*', WIKI.models.groups.relatedQuery('users').count().as('userCount') ) }, /** * FETCH A SINGLE GROUP */ async single(obj, args) { return WIKI.models.groups.query().findById(args.id) } }, GroupMutation: { /** * ASSIGN USER TO GROUP */ async assignUser (obj, args, { req }) { // Check for guest user if (args.userId === 2) { throw new gql.GraphQLError('Cannot assign the Guest user to a group.') } // Check for valid group const grp = await WIKI.models.groups.query().findById(args.groupId) if (!grp) { throw new gql.GraphQLError('Invalid Group ID') } // Check assigned permissions for write:groups if ( WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) && grp.permissions.some(p => { const resType = _.last(p.split(':')) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) }) ) { throw new gql.GraphQLError('You are not authorized to assign a user to this elevated group.') } // Check for valid user const usr = await WIKI.models.users.query().findById(args.userId) if (!usr) { throw new gql.GraphQLError('Invalid User ID') } // Check for existing relation const relExist = await WIKI.models.knex('userGroups').where({ userId: args.userId, groupId: args.groupId }).first() if (relExist) { throw new gql.GraphQLError('User is already assigned to group.') } // Assign user to group await grp.$relatedQuery('users').relate(usr.id) // Revoke tokens for this user WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) return { responseResult: graphHelper.generateSuccess('User has been assigned to group.') } }, /** * CREATE NEW GROUP */ async create (obj, args, { req }) { const group = await WIKI.models.groups.query().insertAndFetch({ name: args.name, permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules), isSystem: false }) await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') return { responseResult: graphHelper.generateSuccess('Group created successfully.'), group } }, /** * DELETE GROUP */ async delete (obj, args) { if (args.id === 1 || args.id === 2) { throw new gql.GraphQLError('Cannot delete this group.') } await WIKI.models.groups.query().deleteById(args.id) WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' }) await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') return { responseResult: graphHelper.generateSuccess('Group has been deleted.') } }, /** * UNASSIGN USER FROM GROUP */ async unassignUser (obj, args) { if (args.userId === 2) { throw new gql.GraphQLError('Cannot unassign Guest user') } if (args.userId === 1 && args.groupId === 1) { throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.') } const grp = await WIKI.models.groups.query().findById(args.groupId) if (!grp) { throw new gql.GraphQLError('Invalid Group ID') } const usr = await WIKI.models.users.query().findById(args.userId) if (!usr) { throw new gql.GraphQLError('Invalid User ID') } await grp.$relatedQuery('users').unrelate().where('userId', usr.id) WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) return { responseResult: graphHelper.generateSuccess('User has been unassigned from group.') } }, /** * UPDATE GROUP */ async update (obj, args, { req }) { // Check for unsafe regex page rules if (_.some(args.pageRules, pr => { return pr.match === 'REGEX' && !safeRegex(pr.path) })) { throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.') } // Set default redirect on login value if (_.isEmpty(args.redirectOnLogin)) { args.redirectOnLogin = '/' } // Check assigned permissions for write:groups if ( WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) && args.permissions.some(p => { const resType = _.last(p.split(':')) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) }) ) { throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.') } // Check assigned permissions for manage:groups if ( WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) && args.permissions.some(p => _.last(p.split(':')) === 'system') ) { throw new gql.GraphQLError('You are not authorized to manage this group or assign the manage:system permissions.') } // Update group await WIKI.models.groups.query().patch({ name: args.name, redirectOnLogin: args.redirectOnLogin, permissions: JSON.stringify(args.permissions), pageRules: JSON.stringify(args.pageRules) }).where('id', args.id) // Revoke tokens for this group WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' }) // Reload group permissions await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') return { responseResult: graphHelper.generateSuccess('Group has been updated.') } } }, Group: { users (grp) { return grp.$relatedQuery('users') } } } ================================================ FILE: server/graph/resolvers/localization.js ================================================ const graphHelper = require('../../helpers/graph') const _ = require('lodash') /* global WIKI */ module.exports = { Query: { async localization() { return {} } }, Mutation: { async localization() { return {} } }, LocalizationQuery: { async locales(obj, args, context, info) { let remoteLocales = await WIKI.cache.get('locales') let localLocales = await WIKI.models.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'availability') remoteLocales = remoteLocales || localLocales return _.map(remoteLocales, rl => { let isInstalled = _.some(localLocales, ['code', rl.code]) return { ...rl, isInstalled, installDate: isInstalled ? _.find(localLocales, ['code', rl.code]).updatedAt : null } }) }, async config(obj, args, context, info) { return { locale: WIKI.config.lang.code, autoUpdate: WIKI.config.lang.autoUpdate, namespacing: WIKI.config.lang.namespacing, namespaces: WIKI.config.lang.namespaces } }, translations (obj, args, context, info) { return WIKI.lang.getByNamespace(args.locale, args.namespace) } }, LocalizationMutation: { async downloadLocale(obj, args, context) { try { const job = await WIKI.scheduler.registerJob({ name: 'fetch-graph-locale', immediate: true }, args.locale) await job.finished return { responseResult: graphHelper.generateSuccess('Locale downloaded successfully') } } catch (err) { return graphHelper.generateError(err) } }, async updateLocale(obj, args, context) { try { WIKI.config.lang.code = args.locale WIKI.config.lang.autoUpdate = args.autoUpdate WIKI.config.lang.namespacing = args.namespacing WIKI.config.lang.namespaces = _.union(args.namespaces, [args.locale]) const newLocale = await WIKI.models.locales.query().select('isRTL').where('code', args.locale).first() WIKI.config.lang.rtl = newLocale.isRTL await WIKI.configSvc.saveToDb(['lang']) await WIKI.lang.setCurrentLocale(args.locale) await WIKI.lang.refreshNamespaces() await WIKI.cache.del('nav:locales') return { responseResult: graphHelper.generateSuccess('Locale config updated') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/logging.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async logging() { return {} } }, Mutation: { async logging() { return {} } }, Subscription: { loggingLiveTrail: { subscribe: () => WIKI.GQLEmitter.asyncIterator('livetrail') } }, LoggingQuery: { async loggers(obj, args, context, info) { let loggers = await WIKI.models.loggers.getLoggers() loggers = loggers.map(logger => { const loggerInfo = _.find(WIKI.data.loggers, ['key', logger.key]) || {} return { ...loggerInfo, ...logger, config: _.sortBy(_.transform(logger.config, (res, value, key) => { const configData = _.get(loggerInfo.props, key, {}) res.push({ key, value: JSON.stringify({ ...configData, value }) }) }, []), 'key') } }) // if (args.filter) { loggers = graphHelper.filter(loggers, args.filter) } if (args.orderBy) { loggers = _.sortBy(loggers, [args.orderBy]) } return loggers } }, LoggingMutation: { async updateLoggers(obj, args, context) { try { for (let logger of args.loggers) { await WIKI.models.loggers.query().patch({ isEnabled: logger.isEnabled, level: logger.level, config: _.reduce(logger.config, (result, value, key) => { _.set(result, `${value.key}`, value.value) return result }, {}) }).where('key', logger.key) } return { responseResult: graphHelper.generateSuccess('Loggers updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/mail.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async mail() { return {} } }, Mutation: { async mail() { return {} } }, MailQuery: { async config(obj, args, context, info) { return { ...WIKI.config.mail, pass: WIKI.config.mail.pass.length > 0 ? '********' : '' } } }, MailMutation: { async sendTest(obj, args, context) { try { if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) { throw new WIKI.Error.MailInvalidRecipient() } await WIKI.mail.send({ template: 'test', to: args.recipientEmail, subject: 'A test email from your wiki', text: 'This is a test email sent from your wiki.', data: { preheadertext: 'This is a test email sent from your wiki.' } }) return { responseResult: graphHelper.generateSuccess('Test email sent successfully.') } } catch (err) { return graphHelper.generateError(err) } }, async updateConfig(obj, args, context) { try { WIKI.config.mail = { senderName: args.senderName, senderEmail: args.senderEmail, host: args.host, port: args.port, name: args.name, secure: args.secure, verifySSL: args.verifySSL, user: args.user, pass: (args.pass === '********') ? WIKI.config.mail.pass : args.pass, useDKIM: args.useDKIM, dkimDomainName: args.dkimDomainName, dkimKeySelector: args.dkimKeySelector, dkimPrivateKey: args.dkimPrivateKey } await WIKI.configSvc.saveToDb(['mail']) WIKI.mail.init() return { responseResult: graphHelper.generateSuccess('Mail configuration updated successfully.') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/navigation.js ================================================ const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async navigation () { return {} } }, Mutation: { async navigation () { return {} } }, NavigationQuery: { async tree (obj, args, context, info) { return WIKI.models.navigation.getTree({ cache: false, locale: 'all', bypassAuth: true }) }, config (obj, args, context, info) { return WIKI.config.nav } }, NavigationMutation: { async updateTree (obj, args, context) { try { await WIKI.models.navigation.query().patch({ config: args.tree }).where('key', 'site') for (const tree of args.tree) { await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300) } return { responseResult: graphHelper.generateSuccess('Navigation updated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async updateConfig (obj, args, context) { try { WIKI.config.nav = { mode: args.mode } await WIKI.configSvc.saveToDb(['nav']) return { responseResult: graphHelper.generateSuccess('Navigation config updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/page.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async pages() { return {} } }, Mutation: { async pages() { return {} } }, PageQuery: { /** * PAGE HISTORY */ async history(obj, args, context, info) { const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.id) if (WIKI.auth.checkAccess(context.req.user, ['read:history'], { path: page.path, locale: page.localeCode })) { return WIKI.models.pageHistory.getHistory({ pageId: args.id, offsetPage: args.offsetPage || 0, offsetSize: args.offsetSize || 100 }) } else { throw new WIKI.Error.PageHistoryForbidden() } }, /** * PAGE VERSION */ async version(obj, args, context, info) { const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId) if (WIKI.auth.checkAccess(context.req.user, ['read:history'], { path: page.path, locale: page.localeCode })) { return WIKI.models.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId }) } else { throw new WIKI.Error.PageHistoryForbidden() } }, /** * SEARCH PAGES */ async search (obj, args, context) { if (WIKI.data.searchEngine) { const resp = await WIKI.data.searchEngine.query(args.query, args) return { ...resp, results: _.filter(resp.results, r => { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: r.path, locale: r.locale, tags: r.tags // Tags are needed since access permissions can be limited by page tags too }) }) } } else { return { results: [], suggestions: [], totalHits: 0 } } }, /** * LIST PAGES */ async list (obj, args, context, info) { let results = await WIKI.models.pages.query().column([ 'pages.id', 'path', { locale: 'localeCode' }, 'title', 'description', 'isPublished', 'isPrivate', 'privateNS', 'contentType', 'createdAt', 'updatedAt' ]) .withGraphJoined('tags') .modifyGraph('tags', builder => { builder.select('tag') }) .modify(queryBuilder => { if (args.limit) { queryBuilder.limit(args.limit) } if (args.locale) { queryBuilder.where('localeCode', args.locale) } if (args.creatorId && args.authorId && args.creatorId > 0 && args.authorId > 0) { queryBuilder.where(function () { this.where('creatorId', args.creatorId).orWhere('authorId', args.authorId) }) } else { if (args.creatorId && args.creatorId > 0) { queryBuilder.where('creatorId', args.creatorId) } if (args.authorId && args.authorId > 0) { queryBuilder.where('authorId', args.authorId) } } if (args.tags && args.tags.length > 0) { queryBuilder.whereIn('tags.tag', args.tags.map(t => _.trim(t).toLowerCase())) } const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc' switch (args.orderBy) { case 'CREATED': queryBuilder.orderBy('createdAt', orderDir) break case 'PATH': queryBuilder.orderBy('path', orderDir) break case 'TITLE': queryBuilder.orderBy('title', orderDir) break case 'UPDATED': queryBuilder.orderBy('updatedAt', orderDir) break default: queryBuilder.orderBy('pages.id', orderDir) break } }) results = _.filter(results, r => { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: r.path, locale: r.locale }) }).map(r => ({ ...r, tags: _.map(r.tags, 'tag') })) if (args.tags && args.tags.length > 0) { results = _.filter(results, r => _.every(args.tags, t => _.includes(r.tags, t))) } return results }, /** * FETCH SINGLE PAGE */ async single (obj, args, context, info) { let page = await WIKI.models.pages.getPageFromDb(args.id) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], { path: page.path, locale: page.localeCode })) { return { ...page, locale: page.localeCode, editor: page.editorKey, scriptJs: page.extra.js, scriptCss: page.extra.css } } else { throw new WIKI.Error.PageViewForbidden() } } else { throw new WIKI.Error.PageNotFound() } }, async singleByPath(obj, args, context, info) { let page = await WIKI.models.pages.getPageFromDb({ path: args.path, locale: args.locale, }); if (page) { if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], { path: page.path, locale: page.localeCode })) { return { ...page, locale: page.localeCode, editor: page.editorKey, scriptJs: page.extra.js, scriptCss: page.extra.css } } else { throw new WIKI.Error.PageViewForbidden() } } else { throw new WIKI.Error.PageNotFound() } }, /** * FETCH TAGS */ async tags (obj, args, context, info) { const pages = await WIKI.models.pages.query() .column([ 'path', { locale: 'localeCode' } ]) .withGraphJoined('tags') const allTags = _.filter(pages, r => { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: r.path, locale: r.locale }) }).flatMap(r => r.tags) return _.orderBy(_.uniqBy(allTags, 'id'), ['tag'], ['asc']) }, /** * SEARCH TAGS */ async searchTags (obj, args, context, info) { const query = _.trim(args.query) const pages = await WIKI.models.pages.query() .column([ 'path', { locale: 'localeCode' } ]) .withGraphJoined('tags') .modifyGraph('tags', builder => { builder.select('tag') }) .modify(queryBuilder => { queryBuilder.andWhere(builderSub => { if (WIKI.config.db.type === 'postgres') { builderSub.where('tags.tag', 'ILIKE', `%${query}%`) } else { builderSub.where('tags.tag', 'LIKE', `%${query}%`) } }) }) const allTags = _.filter(pages, r => { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: r.path, locale: r.locale }) }).flatMap(r => r.tags).map(t => t.tag) return _.uniq(allTags).slice(0, 5) }, /** * FETCH PAGE TREE */ async tree (obj, args, context, info) { let curPage = null if (!args.locale) { args.locale = WIKI.config.lang.code } if (args.path && !args.parent) { curPage = await WIKI.models.knex('pageTree').first('parent', 'ancestors').where({ path: args.path, localeCode: args.locale }) if (curPage) { args.parent = curPage.parent || 0 } else { return [] } } const results = await WIKI.models.knex('pageTree').where(builder => { builder.where('localeCode', args.locale) switch (args.mode) { case 'FOLDERS': builder.andWhere('isFolder', true) break case 'PAGES': builder.andWhereNotNull('pageId') break } if (!args.parent || args.parent < 1) { builder.whereNull('parent') } else { builder.where('parent', args.parent) if (args.includeAncestors && curPage && curPage.ancestors.length > 0) { builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors) } } }).orderBy([{ column: 'isFolder', order: 'desc' }, 'title']) return results.filter(r => { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: r.path, locale: r.localeCode }) }).map(r => ({ ...r, parent: r.parent || 0, locale: r.localeCode })) }, /** * FETCH PAGE LINKS */ async links (obj, args, context, info) { let results if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') { results = await WIKI.models.knex('pages') .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' }) .leftJoin('pageLinks', 'pages.id', 'pageLinks.pageId') .where({ 'pages.localeCode': args.locale }) .unionAll( WIKI.models.knex('pageLinks') .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' }) .leftJoin('pages', 'pageLinks.pageId', 'pages.id') .where({ 'pages.localeCode': args.locale }) ) } else { results = await WIKI.models.knex('pages') .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' }) .fullOuterJoin('pageLinks', 'pages.id', 'pageLinks.pageId') .where({ 'pages.localeCode': args.locale }) } return _.reduce(results, (result, val) => { // -> Check if user has access to source and linked page if ( !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.path, locale: args.locale }) || !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.link, locale: val.locale }) ) { return result } const existingEntry = _.findIndex(result, ['id', val.id]) if (existingEntry >= 0) { if (val.link) { result[existingEntry].links.push(`${val.locale}/${val.link}`) } } else { result.push({ id: val.id, title: val.title, path: `${args.locale}/${val.path}`, links: val.link ? [`${val.locale}/${val.link}`] : [] }) } return result }, []) }, /** * CHECK FOR EDITING CONFLICT */ async checkConflicts (obj, args, context, info) { let page = await WIKI.models.pages.query().select('path', 'localeCode', 'updatedAt').findById(args.id) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], { path: page.path, locale: page.localeCode })) { return page.updatedAt > args.checkoutDate } else { throw new WIKI.Error.PageUpdateForbidden() } } else { throw new WIKI.Error.PageNotFound() } }, /** * FETCH LATEST VERSION FOR CONFLICT COMPARISON */ async conflictLatest (obj, args, context, info) { let page = await WIKI.models.pages.getPageFromDb(args.id) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], { path: page.path, locale: page.localeCode })) { return { ...page, tags: page.tags.map(t => t.tag), locale: page.localeCode } } else { throw new WIKI.Error.PageViewForbidden() } } else { throw new WIKI.Error.PageNotFound() } } }, PageMutation: { /** * CREATE PAGE */ async create(obj, args, context) { try { const page = await WIKI.models.pages.createPage({ ...args, user: context.req.user }) return { responseResult: graphHelper.generateSuccess('Page created successfully.'), page } } catch (err) { return graphHelper.generateError(err) } }, /** * UPDATE PAGE */ async update(obj, args, context) { try { const page = await WIKI.models.pages.updatePage({ ...args, user: context.req.user }) return { responseResult: graphHelper.generateSuccess('Page has been updated.'), page } } catch (err) { return graphHelper.generateError(err) } }, /** * CONVERT PAGE */ async convert(obj, args, context) { try { await WIKI.models.pages.convertPage({ ...args, user: context.req.user }) return { responseResult: graphHelper.generateSuccess('Page has been converted.') } } catch (err) { return graphHelper.generateError(err) } }, /** * MOVE PAGE */ async move(obj, args, context) { try { await WIKI.models.pages.movePage({ ...args, user: context.req.user }) return { responseResult: graphHelper.generateSuccess('Page has been moved.') } } catch (err) { return graphHelper.generateError(err) } }, /** * DELETE PAGE */ async delete(obj, args, context) { try { await WIKI.models.pages.deletePage({ ...args, user: context.req.user }) return { responseResult: graphHelper.generateSuccess('Page has been deleted.') } } catch (err) { return graphHelper.generateError(err) } }, /** * DELETE TAG */ async deleteTag (obj, args, context) { try { const tagToDel = await WIKI.models.tags.query().findById(args.id) if (tagToDel) { await tagToDel.$relatedQuery('pages').unrelate() await WIKI.models.tags.query().deleteById(args.id) } else { throw new Error('This tag does not exist.') } return { responseResult: graphHelper.generateSuccess('Tag has been deleted.') } } catch (err) { return graphHelper.generateError(err) } }, /** * UPDATE TAG */ async updateTag (obj, args, context) { try { const affectedRows = await WIKI.models.tags.query() .findById(args.id) .patch({ tag: _.trim(args.tag).toLowerCase(), title: _.trim(args.title) }) if (affectedRows < 1) { throw new Error('This tag does not exist.') } return { responseResult: graphHelper.generateSuccess('Tag has been updated successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * FLUSH PAGE CACHE */ async flushCache(obj, args, context) { try { await WIKI.models.pages.flushCache() WIKI.events.outbound.emit('flushCache') return { responseResult: graphHelper.generateSuccess('Pages Cache has been flushed successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE */ async migrateToLocale(obj, args, context) { try { const count = await WIKI.models.pages.migrateToLocale(args) return { responseResult: graphHelper.generateSuccess('Migrated content to target locale successfully.'), count } } catch (err) { return graphHelper.generateError(err) } }, /** * REBUILD TREE */ async rebuildTree(obj, args, context) { try { await WIKI.models.pages.rebuildTree() return { responseResult: graphHelper.generateSuccess('Page tree rebuilt successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * RENDER PAGE */ async render (obj, args, context) { try { const page = await WIKI.models.pages.query().findById(args.id) if (!page) { throw new WIKI.Error.PageNotFound() } await WIKI.models.pages.renderPage(page) return { responseResult: graphHelper.generateSuccess('Page rendered successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * RESTORE PAGE VERSION */ async restore (obj, args, context) { try { const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId) if (!page) { throw new WIKI.Error.PageNotFound() } if (!WIKI.auth.checkAccess(context.req.user, ['write:pages'], { path: page.path, locale: page.localeCode })) { throw new WIKI.Error.PageRestoreForbidden() } const targetVersion = await WIKI.models.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId }) if (!targetVersion) { throw new WIKI.Error.PageNotFound() } await WIKI.models.pages.updatePage({ ...targetVersion, id: targetVersion.pageId, user: context.req.user, action: 'restored' }) return { responseResult: graphHelper.generateSuccess('Page version restored successfully.') } } catch (err) { return graphHelper.generateError(err) } }, /** * Purge history */ async purgeHistory (obj, args, context) { try { await WIKI.models.pageHistory.purge(args.olderThan) return { responseResult: graphHelper.generateSuccess('Page history purged successfully.') } } catch (err) { return graphHelper.generateError(err) } } }, Page: { async tags (obj) { return WIKI.models.pages.relatedQuery('tags').for(obj.id) } // comments(pg) { // return pg.$relatedQuery('comments') // } } } ================================================ FILE: server/graph/resolvers/rendering.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async rendering() { return {} } }, Mutation: { async rendering() { return {} } }, RenderingQuery: { async renderers(obj, args, context, info) { let renderers = await WIKI.models.renderers.getRenderers() renderers = renderers.map(rdr => { const rendererInfo = _.find(WIKI.data.renderers, ['key', rdr.key]) || {} return { ...rendererInfo, ...rdr, config: _.sortBy(_.transform(rdr.config, (res, value, key) => { const configData = _.get(rendererInfo.props, key, false) if (configData) { res.push({ key, value: JSON.stringify({ ...configData, value }) }) } }, []), 'key') } }) // if (args.filter) { renderers = graphHelper.filter(renderers, args.filter) } if (args.orderBy) { renderers = _.sortBy(renderers, [args.orderBy]) } return renderers } }, RenderingMutation: { async updateRenderers(obj, args, context) { try { for (let rdr of args.renderers) { await WIKI.models.renderers.query().patch({ isEnabled: rdr.isEnabled, config: _.reduce(rdr.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) return result }, {}) }).where('key', rdr.key) } return { responseResult: graphHelper.generateSuccess('Renderers updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/search.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async search() { return {} } }, Mutation: { async search() { return {} } }, SearchQuery: { async searchEngines(obj, args, context, info) { let searchEngines = await WIKI.models.searchEngines.getSearchEngines() searchEngines = searchEngines.map(searchEngine => { const searchEngineInfo = _.find(WIKI.data.searchEngines, ['key', searchEngine.key]) || {} return { ...searchEngineInfo, ...searchEngine, config: _.sortBy(_.transform(searchEngine.config, (res, value, key) => { const configData = _.get(searchEngineInfo.props, key, false) if (configData) { res.push({ key, value: JSON.stringify({ ...configData, value }) }) } }, []), 'key') } }) // if (args.filter) { searchEngines = graphHelper.filter(searchEngines, args.filter) } if (args.orderBy) { searchEngines = _.sortBy(searchEngines, [args.orderBy]) } return searchEngines } }, SearchMutation: { async updateSearchEngines(obj, args, context) { try { let newActiveEngine = '' for (let searchEngine of args.engines) { if (searchEngine.isEnabled) { newActiveEngine = searchEngine.key } await WIKI.models.searchEngines.query().patch({ isEnabled: searchEngine.isEnabled, config: _.reduce(searchEngine.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) return result }, {}) }).where('key', searchEngine.key) } if (newActiveEngine !== WIKI.data.searchEngine.key) { try { await WIKI.data.searchEngine.deactivate() } catch (err) { WIKI.logger.warn('Failed to deactivate previous search engine:', err) } } await WIKI.models.searchEngines.initEngine({ activate: true }) return { responseResult: graphHelper.generateSuccess('Search Engines updated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async rebuildIndex (obj, args, context) { try { await WIKI.data.searchEngine.rebuild() return { responseResult: graphHelper.generateSuccess('Index rebuilt successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/site.js ================================================ const graphHelper = require('../../helpers/graph') const _ = require('lodash') /* global WIKI */ module.exports = { Query: { async site() { return {} } }, Mutation: { async site() { return {} } }, SiteQuery: { async config(obj, args, context, info) { return { host: WIKI.config.host, title: WIKI.config.title, company: WIKI.config.company, contentLicense: WIKI.config.contentLicense, footerOverride: WIKI.config.footerOverride, logoUrl: WIKI.config.logoUrl, pageExtensions: WIKI.config.pageExtensions.join(', '), ...WIKI.config.seo, ...WIKI.config.editShortcuts, ...WIKI.config.features, ...WIKI.config.security, authAutoLogin: WIKI.config.auth.autoLogin, authEnforce2FA: WIKI.config.auth.enforce2FA, authHideLocal: WIKI.config.auth.hideLocal, authLoginBgUrl: WIKI.config.auth.loginBgUrl, authJwtAudience: WIKI.config.auth.audience, authJwtExpiration: WIKI.config.auth.tokenExpiration, authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal, uploadMaxFileSize: WIKI.config.uploads.maxFileSize, uploadMaxFiles: WIKI.config.uploads.maxFiles, uploadScanSVG: WIKI.config.uploads.scanSVG, uploadForceDownload: WIKI.config.uploads.forceDownload } } }, SiteMutation: { async updateConfig(obj, args, context) { try { if (args.hasOwnProperty('host')) { let siteHost = _.trim(args.host) if (siteHost.endsWith('/')) { siteHost = siteHost.slice(0, -1) } WIKI.config.host = siteHost } if (args.hasOwnProperty('title')) { WIKI.config.title = _.trim(args.title) } if (args.hasOwnProperty('company')) { WIKI.config.company = _.trim(args.company) } if (args.hasOwnProperty('contentLicense')) { WIKI.config.contentLicense = args.contentLicense } if (args.hasOwnProperty('footerOverride')) { WIKI.config.footerOverride = args.footerOverride } if (args.hasOwnProperty('logoUrl')) { WIKI.config.logoUrl = _.trim(args.logoUrl) } if (args.hasOwnProperty('pageExtensions')) { WIKI.config.pageExtensions = _.trim(args.pageExtensions).split(',').map(p => p.trim().toLowerCase()).filter(p => p !== '') } WIKI.config.seo = { description: _.get(args, 'description', WIKI.config.seo.description), robots: _.get(args, 'robots', WIKI.config.seo.robots), analyticsService: _.get(args, 'analyticsService', WIKI.config.seo.analyticsService), analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId) } WIKI.config.auth = { autoLogin: _.get(args, 'authAutoLogin', WIKI.config.auth.autoLogin), enforce2FA: _.get(args, 'authEnforce2FA', WIKI.config.auth.enforce2FA), hideLocal: _.get(args, 'authHideLocal', WIKI.config.auth.hideLocal), loginBgUrl: _.get(args, 'authLoginBgUrl', WIKI.config.auth.loginBgUrl), audience: _.get(args, 'authJwtAudience', WIKI.config.auth.audience), tokenExpiration: _.get(args, 'authJwtExpiration', WIKI.config.auth.tokenExpiration), tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal) } WIKI.config.editShortcuts = { editFab: _.get(args, 'editFab', WIKI.config.editShortcuts.editFab), editMenuBar: _.get(args, 'editMenuBar', WIKI.config.editShortcuts.editMenuBar), editMenuBtn: _.get(args, 'editMenuBtn', WIKI.config.editShortcuts.editMenuBtn), editMenuExternalBtn: _.get(args, 'editMenuExternalBtn', WIKI.config.editShortcuts.editMenuExternalBtn), editMenuExternalName: _.get(args, 'editMenuExternalName', WIKI.config.editShortcuts.editMenuExternalName), editMenuExternalIcon: _.get(args, 'editMenuExternalIcon', WIKI.config.editShortcuts.editMenuExternalIcon), editMenuExternalUrl: _.get(args, 'editMenuExternalUrl', WIKI.config.editShortcuts.editMenuExternalUrl) } WIKI.config.features = { featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings), featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments), featurePersonalWikis: _.get(args, 'featurePersonalWikis', WIKI.config.features.featurePersonalWikis) } WIKI.config.security = { securityOpenRedirect: _.get(args, 'securityOpenRedirect', WIKI.config.security.securityOpenRedirect), securityIframe: _.get(args, 'securityIframe', WIKI.config.security.securityIframe), securityReferrerPolicy: _.get(args, 'securityReferrerPolicy', WIKI.config.security.securityReferrerPolicy), securityTrustProxy: _.get(args, 'securityTrustProxy', WIKI.config.security.securityTrustProxy), securitySRI: _.get(args, 'securitySRI', WIKI.config.security.securitySRI), securityHSTS: _.get(args, 'securityHSTS', WIKI.config.security.securityHSTS), securityHSTSDuration: _.get(args, 'securityHSTSDuration', WIKI.config.security.securityHSTSDuration), securityCSP: _.get(args, 'securityCSP', WIKI.config.security.securityCSP), securityCSPDirectives: _.get(args, 'securityCSPDirectives', WIKI.config.security.securityCSPDirectives) } WIKI.config.uploads = { maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize), maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles), scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG), forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload) } await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'footerOverride', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'editShortcuts', 'features', 'security', 'uploads']) if (WIKI.config.security.securityTrustProxy) { WIKI.app.enable('trust proxy') } else { WIKI.app.disable('trust proxy') } return { responseResult: graphHelper.generateSuccess('Site configuration updated successfully') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/storage.js ================================================ const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ module.exports = { Query: { async storage() { return {} } }, Mutation: { async storage() { return {} } }, StorageQuery: { async targets(obj, args, context, info) { let targets = await WIKI.models.storage.getTargets() targets = _.sortBy(targets.map(tgt => { const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {} return { ...targetInfo, ...tgt, hasSchedule: (targetInfo.schedule !== false), syncInterval: tgt.syncInterval || targetInfo.schedule || 'P0D', syncIntervalDefault: targetInfo.schedule, config: _.sortBy(_.transform(tgt.config, (res, value, key) => { const configData = _.get(targetInfo.props, key, false) if (configData) { res.push({ key, value: JSON.stringify({ ...configData, value: (configData.sensitive && value.length > 0) ? '********' : value }) }) } }, []), 'key') } }), ['title', 'key']) return targets }, async status(obj, args, context, info) { let activeTargets = await WIKI.models.storage.query().where('isEnabled', true) return activeTargets.map(tgt => { const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {} return { key: tgt.key, title: targetInfo.title, status: _.get(tgt, 'state.status', 'pending'), message: _.get(tgt, 'state.message', 'Initializing...'), lastAttempt: _.get(tgt, 'state.lastAttempt', null) } }) } }, StorageMutation: { async updateTargets(obj, args, context) { try { let dbTargets = await WIKI.models.storage.getTargets() for (let tgt of args.targets) { const currentDbTarget = _.find(dbTargets, ['key', tgt.key]) if (!currentDbTarget) { continue } await WIKI.models.storage.query().patch({ isEnabled: tgt.isEnabled, mode: tgt.mode, syncInterval: tgt.syncInterval, config: _.reduce(tgt.config, (result, value, key) => { let configValue = _.get(JSON.parse(value.value), 'v', null) if (configValue === '********') { configValue = _.get(currentDbTarget.config, value.key, '') } _.set(result, `${value.key}`, configValue) return result }, {}), state: { status: 'pending', message: 'Initializing...', lastAttempt: null } }).where('key', tgt.key) } await WIKI.models.storage.initTargets() return { responseResult: graphHelper.generateSuccess('Storage targets updated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async executeAction(obj, args, context) { try { await WIKI.models.storage.executeAction(args.targetKey, args.handler) return { responseResult: graphHelper.generateSuccess('Action completed.') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/system.js ================================================ const _ = require('lodash') const getos = require('getos') const os = require('os') const filesize = require('filesize') const path = require('path') const fs = require('fs-extra') const moment = require('moment') const graphHelper = require('../../helpers/graph') const request = require('request-promise') const crypto = require('crypto') const nanoid = require('nanoid/non-secure').customAlphabet('1234567890abcdef', 10) const getosAsync = require('util').promisify(getos) /* global WIKI */ const dbTypes = { mysql: 'MySQL', mariadb: 'MariaDB', postgres: 'PostgreSQL', sqlite: 'SQLite', mssql: 'MS SQL Server' } module.exports = { Query: { async system () { return {} } }, Mutation: { async system () { return {} } }, SystemQuery: { flags () { return _.transform(WIKI.config.flags, (result, value, key) => { result.push({ key, value }) }, []) }, async info () { return {} }, async extensions () { const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled'])) for (let ext of exts) { ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible() } return exts }, async exportStatus () { return { status: WIKI.system.exportStatus.status, progress: Math.ceil(WIKI.system.exportStatus.progress), message: WIKI.system.exportStatus.message, startedAt: WIKI.system.exportStatus.startedAt } } }, SystemMutation: { async updateFlags (obj, args, context) { WIKI.config.flags = _.transform(args.flags, (result, row) => { _.set(result, row.key, row.value) }, {}) await WIKI.configSvc.applyFlags() await WIKI.configSvc.saveToDb(['flags']) return { responseResult: graphHelper.generateSuccess('System Flags applied successfully') } }, async resetTelemetryClientId (obj, args, context) { try { WIKI.telemetry.generateClientId() await WIKI.configSvc.saveToDb(['telemetry']) return { responseResult: graphHelper.generateSuccess('Telemetry state updated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async setTelemetry (obj, args, context) { try { _.set(WIKI.config, 'telemetry.isEnabled', args.enabled) WIKI.telemetry.enabled = args.enabled await WIKI.configSvc.saveToDb(['telemetry']) return { responseResult: graphHelper.generateSuccess('Telemetry Client ID has been reset successfully') } } catch (err) { return graphHelper.generateError(err) } }, async performUpgrade (obj, args, context) { try { if (process.env.UPGRADE_COMPANION) { await request({ method: 'POST', uri: 'http://wiki-update-companion/upgrade', qs: { ...process.env.UPGRADE_COMPANION_REF && { container: process.env.UPGRADE_COMPANION_REF } } }) return { responseResult: graphHelper.generateSuccess('Upgrade has started.') } } else { throw new Error('You must run the wiki-update-companion container and pass the UPGRADE_COMPANION env var in order to use this feature.') } } catch (err) { return graphHelper.generateError(err) } }, /** * Import Users from a v1 installation */ async importUsersFromV1(obj, args, context) { try { const MongoClient = require('mongodb').MongoClient if (args.mongoDbConnString && args.mongoDbConnString.length > 10) { // -> Connect to DB const client = await MongoClient.connect(args.mongoDbConnString, { appname: `Wiki.js ${WIKI.version} Migration Tool` }) const dbUsers = client.db().collection('users') const userCursor = dbUsers.find({ email: { '$ne': 'guest' } }) const curDateISO = new Date().toISOString() let failed = [] let usersCount = 0 let groupsCount = 0 let assignableGroups = [] let reuseGroups = [] // -> Create SINGLE group if (args.groupMode === `SINGLE`) { const singleGroup = await WIKI.models.groups.query().insert({ name: `Import_${curDateISO}`, permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules) }) groupsCount++ assignableGroups.push(singleGroup.id) } // -> Iterate all users while (await userCursor.hasNext()) { const usr = await userCursor.next() let usrGroup = [] if (args.groupMode === `MULTI`) { // -> Check if global admin if (_.some(usr.rights, ['role', 'admin'])) { usrGroup.push(1) } else { // -> Check if identical group already exists const currentRights = _.sortBy(_.map(usr.rights, r => _.pick(r, ['role', 'path', 'exact', 'deny'])), ['role', 'path', 'exact', 'deny']) const ruleSetId = crypto.createHash('sha1').update(JSON.stringify(currentRights)).digest('base64') const existingGroup = _.find(reuseGroups, ['hash', ruleSetId]) if (existingGroup) { usrGroup.push(existingGroup.groupId) } else { // -> Build new group const pageRules = _.map(usr.rights, r => { let roles = ['read:pages', 'read:assets', 'read:comments', 'write:comments'] if (r.role === `write`) { roles = _.concat(roles, ['write:pages', 'manage:pages', 'read:source', 'read:history', 'write:assets', 'manage:assets']) } return { id: nanoid(), roles: roles, match: r.exact ? 'EXACT' : 'START', deny: r.deny, path: (r.path.indexOf('/') === 0) ? r.path.substring(1) : r.path, locales: [] } }) const perms = _.chain(pageRules).reject('deny').map('roles').union().flatten().value() // -> Create new group const newGroup = await WIKI.models.groups.query().insert({ name: `Import_${curDateISO}_${groupsCount + 1}`, permissions: JSON.stringify(perms), pageRules: JSON.stringify(pageRules) }) reuseGroups.push({ groupId: newGroup.id, hash: ruleSetId }) groupsCount++ usrGroup.push(newGroup.id) } } } // -> Create User try { await WIKI.models.users.createNewUser({ providerKey: usr.provider, email: usr.email, name: usr.name, passwordRaw: usr.password, groups: (usrGroup.length > 0) ? usrGroup : assignableGroups, mustChangePassword: false, sendWelcomeEmail: false }) usersCount++ } catch (err) { failed.push({ provider: usr.provider, email: usr.email, error: err.message }) WIKI.logger.warn(`${usr.email}: ${err}`) } } // -> Reload group permissions if (args.groupMode !== `NONE`) { await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') } client.close() return { responseResult: graphHelper.generateSuccess('Import completed.'), usersCount: usersCount, groupsCount: groupsCount, failed: failed } } else { throw new Error('MongoDB Connection String is missing or invalid.') } } catch (err) { return graphHelper.generateError(err) } }, /** * Set HTTPS Redirection State */ async setHTTPSRedirection (obj, args, context) { _.set(WIKI.config, 'server.sslRedir', args.enabled) await WIKI.configSvc.saveToDb(['server']) return { responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.') } }, /** * Renew SSL Certificate */ async renewHTTPSCertificate (obj, args, context) { try { if (!WIKI.config.ssl.enabled) { throw new WIKI.Error.SystemSSLDisabled() } else if (WIKI.config.ssl.provider !== `letsencrypt`) { throw new WIKI.Error.SystemSSLRenewInvalidProvider() } else if (!WIKI.servers.le) { throw new WIKI.Error.SystemSSLLEUnavailable() } else { await WIKI.servers.le.requestCertificate() await WIKI.servers.restartServer('https') return { responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.') } } } catch (err) { return graphHelper.generateError(err) } }, /** * Export Wiki to Disk */ async export (obj, args, context) { try { const desiredPath = path.resolve(WIKI.ROOTPATH, args.path) // -> Check if export process is already running if (WIKI.system.exportStatus.status === 'running') { throw new Error('Another export is already running.') } // -> Validate entities if (args.entities.length < 1) { throw new Error('Must specify at least 1 entity to export.') } // -> Check target path await fs.ensureDir(desiredPath) const existingFiles = await fs.readdir(desiredPath) if (existingFiles.length) { throw new Error('Target directory must be empty!') } // -> Start export WIKI.system.export({ entities: args.entities, path: desiredPath }) return { responseResult: graphHelper.generateSuccess('Export started successfully.') } } catch (err) { return graphHelper.generateError(err) } } }, SystemInfo: { configFile () { return path.join(process.cwd(), 'config.yml') }, cpuCores () { return os.cpus().length }, currentVersion () { return WIKI.version }, dbType () { return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB') }, async dbVersion () { let version = 'Unknown Version' switch (WIKI.config.db.type) { case 'mariadb': case 'mysql': const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;') version = _.get(resultMYSQL, '[0][0].version', 'Unknown Version') break case 'mssql': const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;') version = _.get(resultMSSQL, '[0].version', 'Unknown Version') break case 'postgres': version = _.get(WIKI.models, 'knex.client.version', 'Unknown Version') break case 'sqlite': version = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown Version') break } return version }, dbHost () { if (WIKI.config.db.type === 'sqlite') { return WIKI.config.db.storage } else { return WIKI.config.db.host } }, hostname () { return os.hostname() }, httpPort () { return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0 }, httpRedirection () { return _.get(WIKI.config, 'server.sslRedir', false) }, httpsPort () { return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0 }, latestVersion () { return WIKI.system.updates.version }, latestVersionReleaseDate () { return moment.utc(WIKI.system.updates.releaseDate) }, nodeVersion () { return process.version.substr(1) }, async operatingSystem () { let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}` if (os.platform() === 'linux') { const osInfo = await getosAsync() osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}` } return osLabel }, async platform () { const isDockerized = await fs.pathExists('/.dockerenv') if (isDockerized) { return 'docker' } return os.platform() }, ramTotal () { return filesize(os.totalmem()) }, sslDomain () { return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.domain : null }, sslExpirationDate () { return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null }, sslProvider () { return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null }, sslStatus () { return 'OK' }, sslSubscriberEmail () { return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.subscriberEmail : null }, telemetry () { return WIKI.telemetry.enabled }, telemetryClientId () { return WIKI.config.telemetry.clientId }, async upgradeCapable () { return !_.isNil(process.env.UPGRADE_COMPANION) }, workingDirectory () { return process.cwd() }, async groupsTotal () { const total = await WIKI.models.groups.query().count('* as total').first() return _.toSafeInteger(total.total) }, async pagesTotal () { const total = await WIKI.models.pages.query().count('* as total').first() return _.toSafeInteger(total.total) }, async usersTotal () { const total = await WIKI.models.users.query().count('* as total').first() return _.toSafeInteger(total.total) }, async tagsTotal () { const total = await WIKI.models.tags.query().count('* as total').first() return _.toSafeInteger(total.total) } } } ================================================ FILE: server/graph/resolvers/tag.js ================================================ module.exports = { // Query: { // tags(obj, args, context, info) { // return WIKI.models.Tag.findAll({ where: args }) // } // }, // Mutation: { // assignTagToDocument(obj, args) { // return WIKI.models.Tag.findById(args.tagId).then(tag => { // if (!tag) { // throw new gql.GraphQLError('Invalid Tag ID') // } // return WIKI.models.Document.findById(args.documentId).then(doc => { // if (!doc) { // throw new gql.GraphQLError('Invalid Document ID') // } // return tag.addDocument(doc) // }) // }) // }, // createTag(obj, args) { // return WIKI.models.Tag.create(args) // }, // deleteTag(obj, args) { // return WIKI.models.Tag.destroy({ // where: { // id: args.id // }, // limit: 1 // }) // }, // removeTagFromDocument(obj, args) { // return WIKI.models.Tag.findById(args.tagId).then(tag => { // if (!tag) { // throw new gql.GraphQLError('Invalid Tag ID') // } // return WIKI.models.Document.findById(args.documentId).then(doc => { // if (!doc) { // throw new gql.GraphQLError('Invalid Document ID') // } // return tag.removeDocument(doc) // }) // }) // }, // renameTag(obj, args) { // return WIKI.models.Group.update({ // key: args.key // }, { // where: { id: args.id } // }) // } // }, // Tag: { // documents(tag) { // return tag.getDocuments() // } // } } ================================================ FILE: server/graph/resolvers/theming.js ================================================ const graphHelper = require('../../helpers/graph') const _ = require('lodash') const CleanCSS = require('clean-css') /* global WIKI */ module.exports = { Query: { async theming() { return {} } }, Mutation: { async theming() { return {} } }, ThemingQuery: { async themes(obj, args, context, info) { return [{ // TODO key: 'default', title: 'Default', author: 'requarks.io' }] }, async config(obj, args, context, info) { return { theme: WIKI.config.theming.theme, iconset: WIKI.config.theming.iconset, darkMode: WIKI.config.theming.darkMode, tocPosition: WIKI.config.theming.tocPosition || 'left', injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles, injectHead: WIKI.config.theming.injectHead, injectBody: WIKI.config.theming.injectBody } } }, ThemingMutation: { async setConfig(obj, args, context, info) { try { if (!_.isEmpty(args.injectCSS)) { args.injectCSS = new CleanCSS({ inline: false }).minify(args.injectCSS).styles } WIKI.config.theming = { ...WIKI.config.theming, theme: args.theme, iconset: args.iconset, darkMode: args.darkMode, tocPosition: args.tocPosition || 'left', injectCSS: args.injectCSS || '', injectHead: args.injectHead || '', injectBody: args.injectBody || '' } await WIKI.configSvc.saveToDb(['theming']) return { responseResult: graphHelper.generateSuccess('Theme config updated') } } catch (err) { return graphHelper.generateError(err) } } } } ================================================ FILE: server/graph/resolvers/user.js ================================================ const graphHelper = require('../../helpers/graph') const _ = require('lodash') /* global WIKI */ module.exports = { Query: { async users() { return {} } }, Mutation: { async users() { return {} } }, UserQuery: { async list(obj, args, context, info) { return WIKI.models.users.query() .select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt') }, async search(obj, args, context, info) { return WIKI.models.users.query() .where('email', 'like', `%${args.query}%`) .orWhere('name', 'like', `%${args.query}%`) .limit(10) .select('id', 'email', 'name', 'providerKey', 'createdAt') }, async single(obj, args, context, info) { let usr = await WIKI.models.users.query().findById(args.id) usr.password = '' usr.tfaSecret = '' const str = _.get(WIKI.auth.strategies, usr.providerKey) str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey]) usr.providerName = str.displayName usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false) return usr }, async profile (obj, args, context, info) { if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { throw new WIKI.Error.AuthRequired() } const usr = await WIKI.models.users.query().findById(context.req.user.id) if (!usr.isActive) { throw new WIKI.Error.AuthAccountBanned() } const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {}) usr.providerName = providerInfo.displayName || 'Unknown' usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt usr.password = '' usr.providerId = '' usr.tfaSecret = '' return usr }, async lastLogins (obj, args, context, info) { return WIKI.models.users.query() .select('id', 'name', 'lastLoginAt') .whereNotNull('lastLoginAt') .orderBy('lastLoginAt', 'desc') .limit(10) } }, UserMutation: { async create (obj, args) { try { await WIKI.models.users.createNewUser(args) return { responseResult: graphHelper.generateSuccess('User created successfully') } } catch (err) { return graphHelper.generateError(err) } }, async delete (obj, args) { try { if (args.id <= 2) { throw new WIKI.Error.UserDeleteProtected() } await WIKI.models.users.deleteUser(args.id, args.replaceId) WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' }) return { responseResult: graphHelper.generateSuccess('User deleted successfully') } } catch (err) { if (err.message.indexOf('foreign') >= 0) { return graphHelper.generateError(new WIKI.Error.UserDeleteForeignConstraint()) } else { return graphHelper.generateError(err) } } }, async update (obj, args) { try { await WIKI.models.users.updateUser(args) return { responseResult: graphHelper.generateSuccess('User created successfully') } } catch (err) { return graphHelper.generateError(err) } }, async verify (obj, args) { try { await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id) return { responseResult: graphHelper.generateSuccess('User verified successfully') } } catch (err) { return graphHelper.generateError(err) } }, async activate (obj, args) { try { await WIKI.models.users.query().patch({ isActive: true }).findById(args.id) return { responseResult: graphHelper.generateSuccess('User activated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async deactivate (obj, args) { try { if (args.id <= 2) { throw new Error('Cannot deactivate system accounts.') } await WIKI.models.users.query().patch({ isActive: false }).findById(args.id) WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' }) return { responseResult: graphHelper.generateSuccess('User deactivated successfully') } } catch (err) { return graphHelper.generateError(err) } }, async enableTFA (obj, args) { try { await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id) return { responseResult: graphHelper.generateSuccess('User 2FA enabled successfully') } } catch (err) { return graphHelper.generateError(err) } }, async disableTFA (obj, args) { try { await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id) return { responseResult: graphHelper.generateSuccess('User 2FA disabled successfully') } } catch (err) { return graphHelper.generateError(err) } }, resetPassword (obj, args) { return false }, async updateProfile (obj, args, context) { try { if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { throw new WIKI.Error.AuthRequired() } const usr = await WIKI.models.users.query().findById(context.req.user.id) if (!usr.isActive) { throw new WIKI.Error.AuthAccountBanned() } if (!usr.isVerified) { throw new WIKI.Error.AuthAccountNotVerified() } if (!['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) { throw new WIKI.Error.InputInvalid() } if (!['', 'light', 'dark'].includes(args.appearance)) { throw new WIKI.Error.InputInvalid() } await WIKI.models.users.updateUser({ id: usr.id, name: _.trim(args.name), jobTitle: _.trim(args.jobTitle), location: _.trim(args.location), timezone: args.timezone, dateFormat: args.dateFormat, appearance: args.appearance }) const newToken = await WIKI.models.users.refreshToken(usr.id) return { responseResult: graphHelper.generateSuccess('User profile updated successfully'), jwt: newToken.token } } catch (err) { return graphHelper.generateError(err) } }, async changePassword (obj, args, context) { try { if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { throw new WIKI.Error.AuthRequired() } const usr = await WIKI.models.users.query().findById(context.req.user.id) if (!usr.isActive) { throw new WIKI.Error.AuthAccountBanned() } if (!usr.isVerified) { throw new WIKI.Error.AuthAccountNotVerified() } if (usr.providerKey !== 'local') { throw new WIKI.Error.AuthProviderInvalid() } try { await usr.verifyPassword(args.current) } catch (err) { throw new WIKI.Error.AuthPasswordInvalid() } await WIKI.models.users.updateUser({ id: usr.id, newPassword: args.new }) const newToken = await WIKI.models.users.refreshToken(usr) return { responseResult: graphHelper.generateSuccess('Password changed successfully'), jwt: newToken.token } } catch (err) { return graphHelper.generateError(err) } } }, User: { groups (usr) { return usr.$relatedQuery('groups') } }, UserProfile: { async groups (usr) { const usrGroups = await usr.$relatedQuery('groups') return usrGroups.map(g => g.name) }, async pagesTotal (usr) { const result = await WIKI.models.pages.query().count('* as total').where('creatorId', usr.id).first() return _.toSafeInteger(result.total) } } } ================================================ FILE: server/graph/scalars/date.js ================================================ const gql = require('graphql') module.exports = { Date: new gql.GraphQLScalarType({ name: 'Date', description: 'ISO date-time string at UTC', parseValue(value) { return new Date(value) }, serialize(value) { return value.toISOString() }, parseLiteral(ast) { if (ast.kind !== gql.Kind.STRING) { throw new TypeError('Date value must be an string!') } return new Date(ast.value) } }) } ================================================ FILE: server/graph/schemas/analytics.graphql ================================================ # =============================================== # ANALYTICS # =============================================== extend type Query { analytics: AnalyticsQuery } extend type Mutation { analytics: AnalyticsMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- """ Queries for Analytics """ type AnalyticsQuery { """ Fetch list of Analytics providers and their configuration """ providers( "Return only active providers" isEnabled: Boolean ): [AnalyticsProvider] @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- """ Mutations for Analytics """ type AnalyticsMutation { """ Update a list of Analytics providers and their configuration """ updateProviders( "List of providers" providers: [AnalyticsProviderInput]! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- """ Analytics Provider """ type AnalyticsProvider { "Is the provider active" isEnabled: Boolean! "Unique identifier for this provider" key: String! "List of configuration properties, formatted as stringified JSON objects" props: [String] "Name of the provider" title: String! "Short description of the provider" description: String "Is the provider available for use" isAvailable: Boolean "Path to the provider logo" logo: String "Website of the provider" website: String "Configuration values for this provider" config: [KeyValuePair] } """ Analytics Configuration Input """ input AnalyticsProviderInput { "Is the provider active" isEnabled: Boolean! "Unique identifier of the provider" key: String! "Configuration values for this provider" config: [KeyValuePairInput] } ================================================ FILE: server/graph/schemas/asset.graphql ================================================ # =============================================== # ASSETS # =============================================== extend type Query { assets: AssetQuery } extend type Mutation { assets: AssetMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type AssetQuery { list( folderId: Int! kind: AssetKind! ): [AssetItem] @auth(requires: ["manage:system", "read:assets"]) folders( parentFolderId: Int! ): [AssetFolder] @auth(requires: ["manage:system", "read:assets"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type AssetMutation { createFolder( parentFolderId: Int! slug: String! name: String ): DefaultResponse @auth(requires: ["manage:system", "write:assets"]) renameAsset( id: Int! filename: String! ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"]) deleteAsset( id: Int! ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"]) flushTempUploads: DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type AssetItem { id: Int! filename: String! ext: String! kind: AssetKind! mime: String! fileSize: Int! metadata: String createdAt: Date! updatedAt: Date! folder: AssetFolder author: User } type AssetFolder { id: Int! slug: String! name: String } enum AssetKind { IMAGE BINARY ALL } ================================================ FILE: server/graph/schemas/authentication.graphql ================================================ # =============================================== # AUTHENTICATION # =============================================== extend type Query { authentication: AuthenticationQuery } extend type Mutation { authentication: AuthenticationMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type AuthenticationQuery { apiKeys: [AuthenticationApiKey] @auth(requires: ["manage:system", "manage:api"]) apiState: Boolean! @auth(requires: ["manage:system", "manage:api"]) strategies: [AuthenticationStrategy] @auth(requires: ["manage:system"]) activeStrategies( enabledOnly: Boolean ): [AuthenticationActiveStrategy] } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type AuthenticationMutation { createApiKey( name: String! expiration: String! fullAccess: Boolean! group: Int ): AuthenticationCreateApiKeyResponse @auth(requires: ["manage:system", "manage:api"]) login( username: String! password: String! strategy: String! ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) loginTFA( continuationToken: String! securityCode: String! setup: Boolean ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) loginChangePassword( continuationToken: String! newPassword: String! ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) forgotPassword( email: String! ): DefaultResponse @rateLimit(limit: 3, duration: 60) register( email: String! password: String! name: String! ): AuthenticationRegisterResponse revokeApiKey( id: Int! ): DefaultResponse @auth(requires: ["manage:system", "manage:api"]) setApiState( enabled: Boolean! ): DefaultResponse @auth(requires: ["manage:system", "manage:api"]) updateStrategies( strategies: [AuthenticationStrategyInput]! ): DefaultResponse @auth(requires: ["manage:system"]) regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"]) resetGuestUser: DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type AuthenticationStrategy { key: String! props: [KeyValuePair] @auth(requires: ["manage:system"]) title: String! description: String isAvailable: Boolean useForm: Boolean! usernameType: String logo: String color: String website: String icon: String } type AuthenticationActiveStrategy { key: String! strategy: AuthenticationStrategy! displayName: String! order: Int! isEnabled: Boolean! config: [KeyValuePair] @auth(requires: ["manage:system"]) selfRegistration: Boolean! domainWhitelist: [String]! @auth(requires: ["manage:system"]) autoEnrollGroups: [Int]! @auth(requires: ["manage:system"]) } type AuthenticationLoginResponse { responseResult: ResponseStatus jwt: String mustChangePwd: Boolean mustProvideTFA: Boolean mustSetupTFA: Boolean continuationToken: String redirect: String tfaQRImage: String } type AuthenticationRegisterResponse { responseResult: ResponseStatus jwt: String } input AuthenticationStrategyInput { key: String! strategyKey: String! config: [KeyValuePairInput] displayName: String! order: Int! isEnabled: Boolean! selfRegistration: Boolean! domainWhitelist: [String]! autoEnrollGroups: [Int]! } type AuthenticationApiKey { id: Int! name: String! keyShort: String! expiration: Date! createdAt: Date! updatedAt: Date! isRevoked: Boolean! } type AuthenticationCreateApiKeyResponse { responseResult: ResponseStatus key: String } ================================================ FILE: server/graph/schemas/comment.graphql ================================================ # =============================================== # COMMENT # =============================================== extend type Query { comments: CommentQuery } extend type Mutation { comments: CommentMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type CommentQuery { providers: [CommentProvider] @auth(requires: ["manage:system"]) list( locale: String! path: String! ): [CommentPost]! @auth(requires: ["read:comments", "manage:system"]) single( id: Int! ): CommentPost @auth(requires: ["read:comments", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type CommentMutation { updateProviders( providers: [CommentProviderInput] ): DefaultResponse @auth(requires: ["manage:system"]) create( pageId: Int! replyTo: Int content: String! guestName: String guestEmail: String ): CommentCreateResponse @auth(requires: ["write:comments", "manage:system"]) @rateLimit(limit: 1, duration: 15) update( id: Int! content: String! ): CommentUpdateResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"]) delete( id: Int! ): DefaultResponse @auth(requires: ["manage:comments", "manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type CommentProvider { isEnabled: Boolean! key: String! title: String! description: String logo: String website: String isAvailable: Boolean config: [KeyValuePair] } input CommentProviderInput { isEnabled: Boolean! key: String! config: [KeyValuePairInput] } type CommentPost { id: Int! content: String! @auth(requires: ["write:comments", "manage:comments", "manage:system"]) render: String! authorId: Int! authorName: String! authorEmail: String! @auth(requires: ["manage:system"]) authorIP: String! @auth(requires: ["manage:system"]) createdAt: Date! updatedAt: Date! } type CommentCreateResponse { responseResult: ResponseStatus id: Int } type CommentUpdateResponse { responseResult: ResponseStatus render: String } ================================================ FILE: server/graph/schemas/common.graphql ================================================ # ====================== # # Wiki.js GraphQL Schema # # ====================== # # DIRECTIVES # ---------- directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION # TYPES # ----- # Generic Key Value Pair type KeyValuePair { key: String! value: String! } # General Key Value Pair Input input KeyValuePairInput { key: String! value: String! } # Generic Mutation Response type DefaultResponse { responseResult: ResponseStatus } # Mutation Status type ResponseStatus { succeeded: Boolean! errorCode: Int! slug: String! message: String } # ROOT # ---- # Query (Read) type Query # Mutations (Create, Update, Delete) type Mutation # Subscriptions (Push, Real-time) type Subscription ================================================ FILE: server/graph/schemas/contribute.graphql ================================================ # =============================================== # CONTRIBUTE # =============================================== extend type Query { contribute: ContributeQuery } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type ContributeQuery { contributors: [ContributeContributor] } # ----------------------------------------------- # TYPES # ----------------------------------------------- type ContributeContributor { id: String! source: String! name: String! joined: Date! website: String twitter: String avatar: String } ================================================ FILE: server/graph/schemas/group.graphql ================================================ # =============================================== # GROUPS # =============================================== extend type Query { groups: GroupQuery } extend type Mutation { groups: GroupMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type GroupQuery { list( filter: String orderBy: String ): [GroupMinimal] @auth(requires: ["write:groups", "manage:groups", "manage:system"]) single( id: Int! ): Group @auth(requires: ["write:groups", "manage:groups", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type GroupMutation { create( name: String! ): GroupResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) update( id: Int! name: String! redirectOnLogin: String! permissions: [String]! pageRules: [PageRuleInput]! ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) delete( id: Int! ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) assignUser( groupId: Int! userId: Int! ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) unassignUser( groupId: Int! userId: Int! ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type GroupResponse { responseResult: ResponseStatus! group: Group } type GroupMinimal { id: Int! name: String! isSystem: Boolean! userCount: Int createdAt: Date! updatedAt: Date! } type Group { id: Int! name: String! isSystem: Boolean! redirectOnLogin: String permissions: [String]! pageRules: [PageRule] users: [UserMinimal] createdAt: Date! updatedAt: Date! } type PageRule { id: String! deny: Boolean! match: PageRuleMatch! roles: [String]! path: String! locales: [String]! } input PageRuleInput { id: String! deny: Boolean! match: PageRuleMatch! roles: [String]! path: String! locales: [String]! } enum PageRuleMatch { START EXACT END REGEX TAG } ================================================ FILE: server/graph/schemas/localization.graphql ================================================ # =============================================== # LOCALIZATION # =============================================== extend type Query { localization: LocalizationQuery } extend type Mutation { localization: LocalizationMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type LocalizationQuery { locales: [LocalizationLocale] config: LocalizationConfig translations(locale: String!, namespace: String!): [Translation] } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type LocalizationMutation { downloadLocale( locale: String! ): DefaultResponse @auth(requires: ["manage:system"]) updateLocale( locale: String! autoUpdate: Boolean! namespacing: Boolean! namespaces: [String]! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type LocalizationLocale { availability: Int! code: String! createdAt: Date! installDate: Date isInstalled: Boolean! isRTL: Boolean! name: String! nativeName: String! updatedAt: Date! } type LocalizationConfig { locale: String! autoUpdate: Boolean! namespacing: Boolean! namespaces: [String]! } type Translation { key: String! value: String! } ================================================ FILE: server/graph/schemas/logging.graphql ================================================ # =============================================== # LOGGING # =============================================== extend type Query { logging: LoggingQuery } extend type Mutation { logging: LoggingMutation } extend type Subscription { loggingLiveTrail: LoggerTrailLine } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type LoggingQuery { loggers( filter: String orderBy: String ): [Logger] @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type LoggingMutation { updateLoggers( loggers: [LoggerInput] ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type Logger { isEnabled: Boolean! key: String! title: String! description: String logo: String website: String level: String config: [KeyValuePair] } input LoggerInput { isEnabled: Boolean! key: String! level: String! config: [KeyValuePairInput] } type LoggerTrailLine { level: String! output: String! timestamp: Date! } ================================================ FILE: server/graph/schemas/mail.graphql ================================================ # =============================================== # MAIL # =============================================== extend type Query { mail: MailQuery } extend type Mutation { mail: MailMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type MailQuery { config: MailConfig @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type MailMutation { sendTest( recipientEmail: String! ): DefaultResponse @auth(requires: ["manage:system"]) updateConfig( senderName: String! senderEmail: String! host: String! port: Int! name: String! secure: Boolean! verifySSL: Boolean! user: String! pass: String! useDKIM: Boolean! dkimDomainName: String! dkimKeySelector: String! dkimPrivateKey: String! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type MailConfig { senderName: String senderEmail: String host: String port: Int name: String secure: Boolean verifySSL: Boolean user: String pass: String useDKIM: Boolean dkimDomainName: String dkimKeySelector: String dkimPrivateKey: String } ================================================ FILE: server/graph/schemas/navigation.graphql ================================================ # =============================================== # NAVIGATION # =============================================== extend type Query { navigation: NavigationQuery } extend type Mutation { navigation: NavigationMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type NavigationQuery { tree: [NavigationTree]! @auth(requires: ["manage:navigation", "manage:system"]) config: NavigationConfig! @auth(requires: ["manage:navigation", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type NavigationMutation { updateTree( tree: [NavigationTreeInput]! ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"]) updateConfig( mode: NavigationMode! ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type NavigationTree { locale: String! items: [NavigationItem]! } input NavigationTreeInput { locale: String! items: [NavigationItemInput]! } type NavigationItem { id: String! kind: String! label: String icon: String targetType: String target: String visibilityMode: String visibilityGroups: [Int] } input NavigationItemInput { id: String! kind: String! label: String icon: String targetType: String target: String visibilityMode: String visibilityGroups: [Int] } type NavigationConfig { mode: NavigationMode! } enum NavigationMode { NONE TREE MIXED STATIC } ================================================ FILE: server/graph/schemas/page.graphql ================================================ # =============================================== # PAGES # =============================================== extend type Query { pages: PageQuery } extend type Mutation { pages: PageMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type PageQuery { history( id: Int! offsetPage: Int offsetSize: Int ): PageHistoryResult @auth(requires: ["manage:system", "read:history"]) version( pageId: Int! versionId: Int! ): PageVersion @auth(requires: ["manage:system", "read:history"]) search( query: String! path: String locale: String ): PageSearchResponse! @auth(requires: ["manage:system", "read:pages"]) list( limit: Int orderBy: PageOrderBy orderByDirection: PageOrderByDirection tags: [String!] locale: String creatorId: Int authorId: Int ): [PageListItem!]! @auth(requires: ["manage:system", "read:pages"]) single( id: Int! ): Page @auth(requires: ["read:pages", "manage:system"]) singleByPath( path: String! locale: String! ): Page @auth(requires: ["read:pages", "manage:system"]) tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"]) searchTags( query: String! ): [String]! @auth(requires: ["manage:system", "read:pages"]) tree( path: String parent: Int mode: PageTreeMode! locale: String! includeAncestors: Boolean ): [PageTreeItem] @auth(requires: ["manage:system", "read:pages"]) links( locale: String! ): [PageLinkItem] @auth(requires: ["manage:system", "read:pages"]) checkConflicts( id: Int! checkoutDate: Date! ): Boolean! @auth(requires: ["write:pages", "manage:pages", "manage:system"]) conflictLatest( id: Int! ): PageConflictLatest! @auth(requires: ["write:pages", "manage:pages", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type PageMutation { create( content: String! description: String! editor: String! isPublished: Boolean! isPrivate: Boolean! locale: String! path: String! publishEndDate: Date publishStartDate: Date scriptCss: String scriptJs: String tags: [String]! title: String! ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) update( id: Int! content: String description: String editor: String isPrivate: Boolean isPublished: Boolean locale: String path: String publishEndDate: Date publishStartDate: Date scriptCss: String scriptJs: String tags: [String] title: String ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) convert( id: Int! editor: String! ): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) move( id: Int! destinationPath: String! destinationLocale: String! ): DefaultResponse @auth(requires: ["manage:pages", "manage:system"]) delete( id: Int! ): DefaultResponse @auth(requires: ["delete:pages", "manage:system"]) deleteTag( id: Int! ): DefaultResponse @auth(requires: ["manage:system"]) updateTag( id: Int! tag: String! title: String! ): DefaultResponse @auth(requires: ["manage:system"]) flushCache: DefaultResponse @auth(requires: ["manage:system"]) migrateToLocale( sourceLocale: String! targetLocale: String! ): PageMigrationResponse @auth(requires: ["manage:system"]) rebuildTree: DefaultResponse @auth(requires: ["manage:system"]) render( id: Int! ): DefaultResponse @auth(requires: ["manage:system"]) restore( pageId: Int! versionId: Int! ): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) purgeHistory ( olderThan: String! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type PageResponse { responseResult: ResponseStatus! page: Page } type PageMigrationResponse { responseResult: ResponseStatus! count: Int } type Page { id: Int! path: String! hash: String! title: String! description: String! isPrivate: Boolean! @auth(requires: ["write:pages", "manage:system"]) isPublished: Boolean! @auth(requires: ["write:pages", "manage:system"]) privateNS: String @auth(requires: ["write:pages", "manage:system"]) publishStartDate: Date! @auth(requires: ["write:pages", "manage:system"]) publishEndDate: Date! @auth(requires: ["write:pages", "manage:system"]) tags: [PageTag]! content: String! @auth(requires: ["read:source", "write:pages", "manage:system"]) render: String toc: String contentType: String! createdAt: Date! updatedAt: Date! editor: String! @auth(requires: ["write:pages", "manage:system"]) locale: String! scriptCss: String scriptJs: String authorId: Int! @auth(requires: ["write:pages", "manage:system"]) authorName: String! @auth(requires: ["write:pages", "manage:system"]) authorEmail: String! @auth(requires: ["write:pages", "manage:system"]) creatorId: Int! @auth(requires: ["write:pages", "manage:system"]) creatorName: String! @auth(requires: ["write:pages", "manage:system"]) creatorEmail: String! @auth(requires: ["write:pages", "manage:system"]) } type PageTag { id: Int! tag: String! title: String createdAt: Date! updatedAt: Date! } type PageHistory { versionId: Int! versionDate: Date! authorId: Int! authorName: String! actionType: String! valueBefore: String valueAfter: String } type PageVersion { action: String! authorId: String! authorName: String! content: String! contentType: String! createdAt: Date! versionDate: Date! description: String! editor: String! isPrivate: Boolean! isPublished: Boolean! locale: String! pageId: Int! path: String! publishEndDate: Date! publishStartDate: Date! tags: [String]! title: String! versionId: Int! } type PageHistoryResult { trail: [PageHistory] total: Int! } type PageSearchResponse { results: [PageSearchResult]! suggestions: [String]! totalHits: Int! } type PageSearchResult { id: String! title: String! description: String! path: String! locale: String! } type PageListItem { id: Int! path: String! locale: String! title: String description: String contentType: String! isPublished: Boolean! isPrivate: Boolean! privateNS: String createdAt: Date! updatedAt: Date! tags: [String] } type PageTreeItem { id: Int! path: String! depth: Int! title: String! isPrivate: Boolean! isFolder: Boolean! privateNS: String parent: Int pageId: Int locale: String! } type PageLinkItem { id: Int! path: String! title: String! links: [String]! } type PageConflictLatest { id: Int! authorId: String! authorName: String! content: String! createdAt: Date! description: String! isPublished: Boolean! locale: String! path: String! tags: [String] title: String! updatedAt: Date! } enum PageOrderBy { CREATED ID PATH TITLE UPDATED } enum PageOrderByDirection { ASC DESC } enum PageTreeMode { FOLDERS PAGES ALL } ================================================ FILE: server/graph/schemas/rendering.graphql ================================================ # =============================================== # RENDERING # =============================================== extend type Query { rendering: RenderingQuery } extend type Mutation { rendering: RenderingMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type RenderingQuery { renderers( filter: String orderBy: String ): [Renderer] @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type RenderingMutation { updateRenderers( renderers: [RendererInput] ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type Renderer { isEnabled: Boolean! key: String! title: String! description: String icon: String dependsOn: String input: String output: String config: [KeyValuePair] } input RendererInput { isEnabled: Boolean! key: String! config: [KeyValuePairInput] } ================================================ FILE: server/graph/schemas/scalars.graphql ================================================ # SCALARS scalar Date ================================================ FILE: server/graph/schemas/search.graphql ================================================ # =============================================== # SEARCH # =============================================== extend type Query { search: SearchQuery } extend type Mutation { search: SearchMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type SearchQuery { searchEngines( filter: String orderBy: String ): [SearchEngine] @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type SearchMutation { updateSearchEngines( engines: [SearchEngineInput] ): DefaultResponse @auth(requires: ["manage:system"]) rebuildIndex: DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type SearchEngine { isEnabled: Boolean! key: String! title: String! description: String logo: String website: String isAvailable: Boolean config: [KeyValuePair] } input SearchEngineInput { isEnabled: Boolean! key: String! config: [KeyValuePairInput] } ================================================ FILE: server/graph/schemas/site.graphql ================================================ # =============================================== # SITE # =============================================== extend type Query { site: SiteQuery } extend type Mutation { site: SiteMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type SiteQuery { config: SiteConfig @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type SiteMutation { updateConfig( host: String title: String description: String robots: [String] analyticsService: String analyticsId: String company: String contentLicense: String footerOverride: String logoUrl: String pageExtensions: String authAutoLogin: Boolean authEnforce2FA: Boolean authHideLocal: Boolean authLoginBgUrl: String authJwtAudience: String authJwtExpiration: String authJwtRenewablePeriod: String editFab: Boolean editMenuBar: Boolean editMenuBtn: Boolean editMenuExternalBtn: Boolean editMenuExternalName: String editMenuExternalIcon: String editMenuExternalUrl: String featurePageRatings: Boolean featurePageComments: Boolean featurePersonalWikis: Boolean securityOpenRedirect: Boolean securityIframe: Boolean securityReferrerPolicy: Boolean securityTrustProxy: Boolean securitySRI: Boolean securityHSTS: Boolean securityHSTSDuration: Int securityCSP: Boolean securityCSPDirectives: String uploadMaxFileSize: Int uploadMaxFiles: Int uploadScanSVG: Boolean uploadForceDownload: Boolean ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type SiteConfig { host: String title: String description: String robots: [String] analyticsService: String analyticsId: String company: String contentLicense: String footerOverride: String logoUrl: String pageExtensions: String authAutoLogin: Boolean authEnforce2FA: Boolean authHideLocal: Boolean authLoginBgUrl: String authJwtAudience: String authJwtExpiration: String authJwtRenewablePeriod: String editFab: Boolean editMenuBar: Boolean editMenuBtn: Boolean editMenuExternalBtn: Boolean editMenuExternalName: String editMenuExternalIcon: String editMenuExternalUrl: String featurePageRatings: Boolean featurePageComments: Boolean featurePersonalWikis: Boolean securityOpenRedirect: Boolean securityIframe: Boolean securityReferrerPolicy: Boolean securityTrustProxy: Boolean securitySRI: Boolean securityHSTS: Boolean securityHSTSDuration: Int securityCSP: Boolean securityCSPDirectives: String uploadMaxFileSize: Int uploadMaxFiles: Int uploadScanSVG: Boolean uploadForceDownload: Boolean } ================================================ FILE: server/graph/schemas/storage.graphql ================================================ # =============================================== # STORAGE # =============================================== extend type Query { storage: StorageQuery } extend type Mutation { storage: StorageMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type StorageQuery { targets: [StorageTarget] @auth(requires: ["manage:system"]) status: [StorageStatus] @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type StorageMutation { updateTargets( targets: [StorageTargetInput]! ): DefaultResponse @auth(requires: ["manage:system"]) executeAction( targetKey: String! handler: String! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type StorageTarget { isAvailable: Boolean! isEnabled: Boolean! key: String! title: String! description: String logo: String website: String supportedModes: [String] mode: String hasSchedule: Boolean! syncInterval: String syncIntervalDefault: String config: [KeyValuePair] actions: [StorageTargetAction] } input StorageTargetInput { isEnabled: Boolean! key: String! mode: String! syncInterval: String config: [KeyValuePairInput] } type StorageStatus { key: String! title: String! status: String! message: String! lastAttempt: String! } type StorageTargetAction { handler: String! label: String! hint: String! } ================================================ FILE: server/graph/schemas/system.graphql ================================================ # =============================================== # SYSTEM # =============================================== extend type Query { system: SystemQuery } extend type Mutation { system: SystemMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type SystemQuery { flags: [SystemFlag] @auth(requires: ["manage:system"]) info: SystemInfo extensions: [SystemExtension] @auth(requires: ["manage:system"]) exportStatus: SystemExportStatus @auth(requires: ["manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type SystemMutation { updateFlags( flags: [SystemFlagInput]! ): DefaultResponse @auth(requires: ["manage:system"]) resetTelemetryClientId: DefaultResponse @auth(requires: ["manage:system"]) setTelemetry( enabled: Boolean! ): DefaultResponse @auth(requires: ["manage:system"]) performUpgrade: DefaultResponse @auth(requires: ["manage:system"]) importUsersFromV1( mongoDbConnString: String! groupMode: SystemImportUsersGroupMode! ): SystemImportUsersResponse @auth(requires: ["manage:system"]) setHTTPSRedirection( enabled: Boolean! ): DefaultResponse @auth(requires: ["manage:system"]) renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"]) export( entities: [String]! path: String! ): DefaultResponse @auth(requires: ["manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type SystemFlag { key: String! value: Boolean! } input SystemFlagInput { key: String! value: Boolean! } type SystemInfo { configFile: String @auth(requires: ["manage:system"]) cpuCores: Int @auth(requires: ["manage:system"]) currentVersion: String @auth(requires: ["manage:system"]) dbHost: String @auth(requires: ["manage:system"]) dbType: String @auth(requires: ["manage:system"]) dbVersion: String @auth(requires: ["manage:system"]) groupsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"]) hostname: String @auth(requires: ["manage:system"]) httpPort: Int @auth(requires: ["manage:system"]) httpRedirection: Boolean @auth(requires: ["manage:system"]) httpsPort: Int @auth(requires: ["manage:system"]) latestVersion: String @auth(requires: ["manage:system"]) latestVersionReleaseDate: Date @auth(requires: ["manage:system"]) nodeVersion: String @auth(requires: ["manage:system"]) operatingSystem: String @auth(requires: ["manage:system"]) pagesTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"]) platform: String @auth(requires: ["manage:system"]) ramTotal: String @auth(requires: ["manage:system"]) sslDomain: String @auth(requires: ["manage:system"]) sslExpirationDate: Date @auth(requires: ["manage:system"]) sslProvider: String @auth(requires: ["manage:system"]) sslStatus: String @auth(requires: ["manage:system"]) sslSubscriberEmail: String @auth(requires: ["manage:system"]) tagsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"]) telemetry: Boolean @auth(requires: ["manage:system"]) telemetryClientId: String @auth(requires: ["manage:system"]) upgradeCapable: Boolean @auth(requires: ["manage:system"]) usersTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"]) workingDirectory: String @auth(requires: ["manage:system"]) } enum SystemImportUsersGroupMode { MULTI SINGLE NONE } type SystemImportUsersResponse { responseResult: ResponseStatus usersCount: Int groupsCount: Int failed: [SystemImportUsersResponseFailed] } type SystemImportUsersResponseFailed { provider: String email: String error: String } type SystemExtension { key: String! title: String! description: String! isInstalled: Boolean! isCompatible: Boolean! } type SystemExportStatus { status: String progress: Int message: String startedAt: Date } ================================================ FILE: server/graph/schemas/theming.graphql ================================================ # =============================================== # THEMES # =============================================== extend type Query { theming: ThemingQuery } extend type Mutation { theming: ThemingMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type ThemingQuery { themes: [ThemingTheme] @auth(requires: ["manage:theme", "manage:system"]) config: ThemingConfig @auth(requires: ["manage:theme", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type ThemingMutation { setConfig( theme: String! iconset: String! darkMode: Boolean! tocPosition: String injectCSS: String injectHead: String injectBody: String ): DefaultResponse @auth(requires: ["manage:theme", "manage:system"]) } # ----------------------------------------------- # TYPES # ----------------------------------------------- type ThemingConfig { theme: String! iconset: String! darkMode: Boolean! tocPosition: String injectCSS: String injectHead: String injectBody: String } type ThemingTheme { key: String title: String author: String } ================================================ FILE: server/graph/schemas/user.graphql ================================================ # =============================================== # USERS # =============================================== extend type Query { users: UserQuery } extend type Mutation { users: UserMutation } # ----------------------------------------------- # QUERIES # ----------------------------------------------- type UserQuery { list( filter: String orderBy: String ): [UserMinimal] @auth(requires: ["write:users", "manage:users", "manage:system"]) search( query: String! ): [UserMinimal] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"]) single( id: Int! ): User @auth(requires: ["manage:users", "manage:system"]) profile: UserProfile lastLogins: [UserLastLogin] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"]) } # ----------------------------------------------- # MUTATIONS # ----------------------------------------------- type UserMutation { create( email: String! name: String! passwordRaw: String providerKey: String! groups: [Int]! mustChangePassword: Boolean sendWelcomeEmail: Boolean ): UserResponse @auth(requires: ["write:users", "manage:users", "manage:system"]) update( id: Int! email: String name: String newPassword: String groups: [Int] location: String jobTitle: String timezone: String dateFormat: String appearance: String ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) delete( id: Int! replaceId: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) verify( id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) activate( id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) deactivate( id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) enableTFA( id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) disableTFA( id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) resetPassword( id: Int! ): DefaultResponse updateProfile( name: String! location: String! jobTitle: String! timezone: String! dateFormat: String! appearance: String! ): UserTokenResponse changePassword( current: String! new: String! ): UserTokenResponse } # ----------------------------------------------- # TYPES # ----------------------------------------------- type UserResponse { responseResult: ResponseStatus! user: User } type UserLastLogin { id: Int! name: String! lastLoginAt: Date! } type UserMinimal { id: Int! name: String! email: String! providerKey: String! isSystem: Boolean! isActive: Boolean! createdAt: Date! lastLoginAt: Date } type User { id: Int! name: String! email: String! providerKey: String! providerName: String providerId: String providerIs2FACapable: Boolean isSystem: Boolean! isActive: Boolean! isVerified: Boolean! location: String! jobTitle: String! timezone: String! dateFormat: String! appearance: String! createdAt: Date! updatedAt: Date! lastLoginAt: Date tfaIsActive: Boolean! groups: [Group]! } type UserProfile { id: Int! name: String! email: String! providerKey: String providerName: String isSystem: Boolean! isVerified: Boolean! location: String! jobTitle: String! timezone: String! dateFormat: String! appearance: String! createdAt: Date! updatedAt: Date! lastLoginAt: Date groups: [String]! pagesTotal: Int! } type UserTokenResponse { responseResult: ResponseStatus! jwt: String } ================================================ FILE: server/helpers/asset.js ================================================ const crypto = require('crypto') const path = require('path') module.exports = { /** * Generate unique hash from page */ generateHash(assetPath) { return crypto.createHash('sha1').update(assetPath).digest('hex') }, getPathInfo(assetPath) { return path.parse(assetPath.toLowerCase()) } } ================================================ FILE: server/helpers/brute-knex.js ================================================ const AbstractClientStore = require('express-brute/lib/AbstractClientStore') const KnexStore = module.exports = function (options) { options = options || Object.create(null) AbstractClientStore.apply(this, arguments) this.options = Object.assign(Object.create(null), KnexStore.defaults, options) if (this.options.knex) { this.knex = this.options.knex } else { this.knex = require('knex')(KnexStore.defaultsKnex) } if (options.createTable === false) { this.ready = Promise.resolve() } else { this.ready = this.knex.schema.hasTable(this.options.tablename) .then((exists) => { if (exists) { return } return this.knex.schema.createTable(this.options.tablename, (table) => { table.string('key') table.bigInteger('firstRequest').nullable() table.bigInteger('lastRequest').nullable() table.bigInteger('lifetime').nullable() table.integer('count') }) }) } } KnexStore.prototype = Object.create(AbstractClientStore.prototype) KnexStore.prototype.set = async function (key, value, lifetime, callback) { try { lifetime = lifetime || 0 await this.ready const resp = await this.knex.transaction((trx) => { return trx .select('*') .forUpdate() .from(this.options.tablename) .where('key', '=', key) .then((foundKeys) => { if (foundKeys.length === 0) { return trx.from(this.options.tablename) .insert({ key: key, lifetime: new Date(Date.now() + lifetime * 1000).getTime(), lastRequest: new Date(value.lastRequest).getTime(), firstRequest: new Date(value.firstRequest).getTime(), count: value.count }) } else { return trx(this.options.tablename) .where('key', '=', key) .update({ lifetime: new Date(Date.now() + lifetime * 1000).getTime(), count: value.count, lastRequest: new Date(value.lastRequest).getTime() }) } }) }) callback(null, resp) } catch (err) { callback(err, null) } } KnexStore.prototype.get = async function (key, callback) { try { await this.ready await this.clearExpired() const resp = await this.knex.select('*') .from(this.options.tablename) .where('key', '=', key) let o = null if (resp[0]) { o = {} o.lastRequest = new Date(resp[0].lastRequest) o.firstRequest = new Date(resp[0].firstRequest) o.count = resp[0].count } callback(null, o) } catch (err) { callback(err, null) } } KnexStore.prototype.reset = async function (key, callback) { try { await this.ready const resp = await this.knex(this.options.tablename) .where('key', '=', key) .del() callback(null, resp) } catch (err) { callback(err, null) } } KnexStore.prototype.increment = async function (key, lifetime, callback) { try { const result = await this.get(key) let resp = null if (result) { resp = await this.knex(this.options.tablename) .increment('count', 1) .where('key', '=', key) } else { resp = await this.knex(this.options.tablename) .insert({ key: key, firstRequest: new Date().getTime(), lastRequest: new Date().getTime(), lifetime: new Date(Date.now() + lifetime * 1000).getTime(), count: 1 }) } callback(null, resp) } catch (err) { callback(err, null) } } KnexStore.prototype.clearExpired = async function (callback) { await this.ready return this.knex(this.options.tablename) .del() .where('lifetime', '<', new Date().getTime()) } KnexStore.defaults = { tablename: 'brute', createTable: true } KnexStore.defaultsKnex = { client: 'sqlite3', // debug: true, connection: { filename: './brute-knex.sqlite' } } ================================================ FILE: server/helpers/common.js ================================================ /* global WIKI */ const _ = require('lodash') const { DateTime } = require('luxon') module.exports = { /** * Get default value of type * * @param {any} type primitive type name * @returns Default value */ getTypeDefaultValue (type) { switch (type.toLowerCase()) { case 'string': return '' case 'number': return 0 case 'boolean': return false } }, parseModuleProps (props) { return _.transform(props, (result, value, key) => { let defaultValue = '' if (_.isPlainObject(value)) { defaultValue = !_.isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type) } else { defaultValue = this.getTypeDefaultValue(value) } _.set(result, key, { default: defaultValue, type: (value.type || value).toLowerCase(), title: value.title || _.startCase(key), hint: value.hint || false, enum: value.enum || false, multiline: value.multiline || false, sensitive: value.sensitive || false, maxWidth: value.maxWidth || 0, order: value.order || 100 }) return result }, {}) }, getCookieOpts () { return { expires: DateTime.utc().plus({ days: 365 }).toJSDate(), ...(WIKI.config.host.startsWith('https://') ? { secure: true } : {}) } } } ================================================ FILE: server/helpers/config.js ================================================ 'use strict' const _ = require('lodash') const isoDurationReg = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/ module.exports = { /** * Parse configuration value for environment vars * * Replaces `$(ENV_VAR_NAME)` with value of `ENV_VAR_NAME` environment variable. * * Also supports defaults by if provided as `$(ENV_VAR_NAME:default)` * * @param {any} cfg Configuration value * @returns Parse configuration value */ parseConfigValue (cfg) { return _.replace( cfg, /\$\(([A-Z0-9_]+)(?::(.+))?\)/g, (fm, m, d) => { return process.env[m] || d } ) }, isValidDurationString (val) { return isoDurationReg.test(val) } } ================================================ FILE: server/helpers/error.js ================================================ const CustomError = require('custom-error-instance') module.exports = { AssetDeleteForbidden: CustomError('AssetDeleteForbidden', { message: 'You are not authorized to delete this asset.', code: 2003 }), AssetFolderExists: CustomError('AssetFolderExists', { message: 'An asset folder with the same name already exists.', code: 2002 }), AssetGenericError: CustomError('AssetGenericError', { message: 'An unexpected error occured during asset operation.', code: 2001 }), AssetInvalid: CustomError('AssetInvalid', { message: 'This asset does not exist or is invalid.', code: 2004 }), AssetRenameCollision: CustomError('AssetRenameCollision', { message: 'An asset with the same filename in the same folder already exists.', code: 2005 }), AssetRenameForbidden: CustomError('AssetRenameForbidden', { message: 'You are not authorized to rename this asset.', code: 2006 }), AssetRenameInvalid: CustomError('AssetRenameInvalid', { message: 'The new asset filename is invalid.', code: 2007 }), AssetRenameInvalidExt: CustomError('AssetRenameInvalidExt', { message: 'The file extension cannot be changed on an existing asset.', code: 2008 }), AssetRenameTargetForbidden: CustomError('AssetRenameTargetForbidden', { message: 'You are not authorized to rename this asset to the requested name.', code: 2009 }), AuthAccountBanned: CustomError('AuthAccountBanned', { message: 'Your account has been disabled.', code: 1013 }), AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', { message: 'An account already exists using this email address.', code: 1004 }), AuthAccountNotVerified: CustomError('AuthAccountNotVerified', { message: 'You must verify your account before your can login.', code: 1014 }), AuthGenericError: CustomError('AuthGenericError', { message: 'An unexpected error occured during login.', code: 1001 }), AuthLoginFailed: CustomError('AuthLoginFailed', { message: 'Invalid email / username or password.', code: 1002 }), AuthPasswordInvalid: CustomError('AuthPasswordInvalid', { message: 'Password is incorrect.', code: 1020 }), AuthProviderInvalid: CustomError('AuthProviderInvalid', { message: 'Invalid authentication provider.', code: 1003 }), AuthRegistrationDisabled: CustomError('AuthRegistrationDisabled', { message: 'Registration is disabled. Contact your system administrator.', code: 1010 }), AuthRegistrationDomainUnauthorized: CustomError('AuthRegistrationDomainUnauthorized', { message: 'You are not authorized to register. Your domain is not whitelisted.', code: 1011 }), AuthRequired: CustomError('AuthRequired', { message: 'You must be authenticated to access this resource.', code: 1019 }), AuthTFAFailed: CustomError('AuthTFAFailed', { message: 'Incorrect TFA Security Code.', code: 1005 }), AuthTFAInvalid: CustomError('AuthTFAInvalid', { message: 'Invalid TFA Security Code or Login Token.', code: 1006 }), AuthValidationTokenInvalid: CustomError('AuthValidationTokenInvalid', { message: 'Invalid validation token.', code: 1015 }), BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', { message: 'Invalid Brute Force Instance.', code: 1007 }), BruteTooManyAttempts: CustomError('BruteTooManyAttempts', { message: 'Too many attempts! Try again later.', code: 1008 }), CommentContentMissing: CustomError('CommentContentMissing', { message: 'Comment content is missing or too short.', code: 8003 }), CommentGenericError: CustomError('CommentGenericError', { message: 'An unexpected error occured.', code: 8001 }), CommentManageForbidden: CustomError('CommentManageForbidden', { message: 'You are not authorized to manage comments on this page.', code: 8004 }), CommentNotFound: CustomError('CommentNotFound', { message: 'This comment does not exist.', code: 8005 }), CommentPostForbidden: CustomError('CommentPostForbidden', { message: 'You are not authorized to post a comment on this page.', code: 8002 }), CommentViewForbidden: CustomError('CommentViewForbidden', { message: 'You are not authorized to view comments for this page.', code: 8006 }), InputInvalid: CustomError('InputInvalid', { message: 'Input data is invalid.', code: 1012 }), LocaleGenericError: CustomError('LocaleGenericError', { message: 'An unexpected error occured during locale operation.', code: 5001 }), LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', { message: 'Invalid locale or namespace.', code: 5002 }), MailGenericError: CustomError('MailGenericError', { message: 'An unexpected error occured during mail operation.', code: 3001 }), MailInvalidRecipient: CustomError('MailInvalidRecipient', { message: 'The recipient email address is invalid.', code: 3004 }), MailNotConfigured: CustomError('MailNotConfigured', { message: 'The mail configuration is incomplete or invalid.', code: 3002 }), MailTemplateFailed: CustomError('MailTemplateFailed', { message: 'Mail template failed to load.', code: 3003 }), PageCreateForbidden: CustomError('PageCreateForbidden', { message: 'You are not authorized to create this page.', code: 6008 }), PageDeleteForbidden: CustomError('PageDeleteForbidden', { message: 'You are not authorized to delete this page.', code: 6010 }), PageGenericError: CustomError('PageGenericError', { message: 'An unexpected error occured during a page operation.', code: 6001 }), PageDuplicateCreate: CustomError('PageDuplicateCreate', { message: 'Cannot create this page because an entry already exists at the same path.', code: 6002 }), PageEmptyContent: CustomError('PageEmptyContent', { message: 'Page content cannot be empty.', code: 6004 }), PageHistoryForbidden: CustomError('PageHistoryForbidden', { message: 'You are not authorized to view the history of this page.', code: 6012 }), PageIllegalPath: CustomError('PageIllegalPath', { message: 'Page path cannot contains illegal characters.', code: 6005 }), PageMoveForbidden: CustomError('PageMoveForbidden', { message: 'You are not authorized to move this page.', code: 6007 }), PageNotFound: CustomError('PageNotFound', { message: 'This page does not exist.', code: 6003 }), PagePathCollision: CustomError('PagePathCollision', { message: 'Destination page path already exists.', code: 6006 }), PageRestoreForbidden: CustomError('PageRestoreForbidden', { message: 'You are not authorized to restore this page version.', code: 6011 }), PageUpdateForbidden: CustomError('PageUpdateForbidden', { message: 'You are not authorized to update this page.', code: 6009 }), PageViewForbidden: CustomError('PageViewForbidden', { message: 'You are not authorized to view this page.', code: 6013 }), SearchActivationFailed: CustomError('SearchActivationFailed', { message: 'Search Engine activation failed.', code: 4002 }), SearchGenericError: CustomError('SearchGenericError', { message: 'An unexpected error occured during search operation.', code: 4001 }), SystemGenericError: CustomError('SystemGenericError', { message: 'An unexpected error occured.', code: 7001 }), SystemSSLDisabled: CustomError('SystemSSLDisabled', { message: 'SSL is not enabled.', code: 7002 }), SystemSSLLEUnavailable: CustomError('SystemSSLLEUnavailable', { message: 'Let\'s Encrypt is not initialized.', code: 7004 }), SystemSSLRenewInvalidProvider: CustomError('SystemSSLRenewInvalidProvider', { message: 'Current provider does not support SSL certificate renewal.', code: 7003 }), UserCreationFailed: CustomError('UserCreationFailed', { message: 'An unexpected error occured during user creation.', code: 1009 }), UserDeleteForeignConstraint: CustomError('UserDeleteForeignConstraint', { message: 'Cannot delete user because of content relational constraints.', code: 1017 }), UserDeleteProtected: CustomError('UserDeleteProtected', { message: 'Cannot delete a protected system account.', code: 1018 }), UserNotFound: CustomError('UserNotFound', { message: 'This user does not exist.', code: 1016 }) } ================================================ FILE: server/helpers/graph.js ================================================ const _ = require('lodash') module.exports = { generateSuccess (msg) { return { succeeded: true, errorCode: 0, slug: 'ok', message: _.defaultTo(msg, 'Operation succeeded.') } }, generateError (err, complete = true) { const error = { succeeded: false, errorCode: _.isFinite(err.code) ? err.code : 1, slug: err.name, message: err.message || 'An unexpected error occured.' } return (complete) ? { responseResult: error } : error } } ================================================ FILE: server/helpers/page.js ================================================ const qs = require('querystring') const _ = require('lodash') const crypto = require('crypto') const path = require('path') const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i // eslint-disable-next-line no-control-regex const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/ const contentToExt = { markdown: 'md', asciidoc: 'adoc', html: 'html' } const extToContent = _.invert(contentToExt) /* global WIKI */ module.exports = { /** * Parse raw url path and make it safe */ parsePath (rawPath, opts = {}) { let pathObj = { locale: WIKI.config.lang.code, path: 'home', private: false, privateNS: '', explicitLocale: false } // Clean Path rawPath = _.trim(qs.unescape(rawPath)) if (_.startsWith(rawPath, '/')) { rawPath = rawPath.substring(1) } rawPath = rawPath.replace(unsafeCharsRegex, '') if (rawPath === '') { rawPath = 'home' } rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '') // Extract Info let pathParts = _.filter(_.split(rawPath, '/'), p => { p = _.trim(p) return !_.isEmpty(p) && p !== '..' && p !== '.' }) if (pathParts[0].length === 1) { pathParts.shift() } if (localeSegmentRegex.test(pathParts[0])) { pathObj.locale = pathParts[0] pathObj.explicitLocale = true pathParts.shift() } // Strip extension if (opts.stripExt && pathParts.length > 0) { const lastPart = _.last(pathParts) if (lastPart.indexOf('.') > 0) { pathParts.pop() const lastPartMeta = path.parse(lastPart) pathParts.push(lastPartMeta.name) } } pathObj.path = _.join(pathParts, '/') return pathObj }, /** * Generate unique hash from page */ generateHash(opts) { return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex') }, /** * Inject Page Metadata */ injectPageMetadata(page) { let meta = [ ['title', page.title], ['description', page.description], ['published', page.isPublished.toString()], ['date', page.updatedAt], ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''], ['editor', page.editorKey], ['dateCreated', page.createdAt] ] switch (page.contentType) { case 'markdown': return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content case 'html': return '\n\n' + page.content case 'json': return { ...page.content, _meta: _.fromPairs(meta) } default: return page.content } }, /** * Check if path is a reserved path */ isReservedPath(rawPath) { const firstSection = _.head(rawPath.split('/')) if (firstSection.length <= 1) { return true } else if (localeSegmentRegex.test(firstSection)) { return true } else if ( _.some(WIKI.data.reservedPaths, p => { return p === firstSection })) { return true } else { return false } }, /** * Get file extension from content type */ getFileExtension(contentType) { return _.get(contentToExt, contentType, 'txt') }, /** * Get content type from file extension */ getContentType (filePath) { const ext = _.last(filePath.split('.')) return _.get(extToContent, ext, false) }, /** * Get Page Meta object from disk path */ getPagePath (filePath) { let fpath = filePath if (process.platform === 'win32') { fpath = filePath.replace(/\\/g, '/') } let meta = { locale: WIKI.config.lang.code, path: _.initial(fpath.split('.')).join('') } const result = localeFolderRegex.exec(meta.path) if (result[1]) { meta = { locale: result[1].replace('/', ''), path: result[2] } } return meta } } ================================================ FILE: server/helpers/security.js ================================================ const Promise = require('bluebird') const crypto = require('crypto') const passportJWT = require('passport-jwt') module.exports = { sanitizeCommitUser (user) { // let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g') // return { // name: _.chain(user.name).replace(wlist, '').trim().value(), // email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail // } }, /** * Generate a random token * * @param {any} length * @returns */ async generateToken (length) { return Promise.fromCallback(clb => { crypto.randomBytes(length, clb) }).then(buf => { return buf.toString('hex') }) }, extractJWT: passportJWT.ExtractJwt.fromExtractors([ passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(), (req) => { let token = null if (req && req.cookies) { token = req.cookies['jwt'] } // Force uploads to use Auth headers if (req.path.toLowerCase() === '/u') { return null } return token } ]) } ================================================ FILE: server/index.js ================================================ // =========================================== // Wiki.js // Licensed under AGPLv3 // =========================================== const path = require('path') const { nanoid } = require('nanoid') const { DateTime } = require('luxon') const { gte } = require('semver') // ---------------------------------------- // Init WIKI instance // ---------------------------------------- let WIKI = { IS_DEBUG: process.env.NODE_ENV === 'development', IS_MASTER: true, ROOTPATH: process.cwd(), INSTANCE_ID: nanoid(10), SERVERPATH: path.join(process.cwd(), 'server'), Error: require('./helpers/error'), configSvc: require('./core/config'), kernel: require('./core/kernel'), startedAt: DateTime.utc() } global.WIKI = WIKI WIKI.configSvc.init() // ---------------------------------------- // Init Logger // ---------------------------------------- WIKI.logger = require('./core/logger').init('MASTER') // ---------------------------------------- // Start Kernel // ---------------------------------------- WIKI.kernel.init() // ---------------------------------------- // Register exit handler // ---------------------------------------- process.on('SIGTERM', () => { WIKI.kernel.shutdown() }) process.on('SIGINT', () => { WIKI.kernel.shutdown() }) process.on('message', (msg) => { if (msg === 'shutdown') { WIKI.kernel.shutdown() } }) ================================================ FILE: server/jobs/fetch-graph-locale.js ================================================ const _ = require('lodash') const { createApolloFetch } = require('apollo-fetch') /* global WIKI */ module.exports = async (localeCode) => { WIKI.logger.info(`Fetching locale ${localeCode} from Graph endpoint...`) try { const apollo = createApolloFetch({ uri: WIKI.config.graphEndpoint }) const respStrings = await apollo({ query: `query ($code: String!) { localization { strings(code: $code) { key value } } }`, variables: { code: localeCode } }) const strings = _.get(respStrings, 'data.localization.strings', []) let lcObj = {} _.forEach(strings, row => { if (_.includes(row.key, '::')) { return } if (_.isEmpty(row.value)) { row.value = row.key } _.set(lcObj, row.key.replace(':', '.'), row.value) }) const locales = await WIKI.cache.get('locales') if (locales) { const currentLocale = _.find(locales, ['code', localeCode]) || {} const existingLocale = await WIKI.models.locales.query().where('code', localeCode).first() if (existingLocale) { await WIKI.models.locales.query().patch({ strings: lcObj }).where('code', localeCode) } else { await WIKI.models.locales.query().insert({ code: localeCode, strings: lcObj, isRTL: currentLocale.isRTL, name: currentLocale.name, nativeName: currentLocale.nativeName, availability: currentLocale.availability }) } } else { throw new Error('Failed to fetch cached locales list! Restart server to resolve this issue.') } await WIKI.lang.refreshNamespaces() WIKI.logger.info(`Fetching locale ${localeCode} from Graph endpoint: [ COMPLETED ]`) } catch (err) { WIKI.logger.error(`Fetching locale ${localeCode} from Graph endpoint: [ FAILED ]`) WIKI.logger.error(err.message) } } ================================================ FILE: server/jobs/purge-uploads.js ================================================ /* global WIKI */ const Promise = require('bluebird') const fs = require('fs-extra') const moment = require('moment') const path = require('path') module.exports = async () => { WIKI.logger.info('Purging orphaned upload files...') try { const uplTempPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads') await fs.ensureDir(uplTempPath) const ls = await fs.readdir(uplTempPath) const fifteenAgo = moment().subtract(15, 'minutes') await Promise.map(ls, (f) => { return fs.stat(path.join(uplTempPath, f)).then((s) => { return { filename: f, stat: s } }) }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => { return Promise.map(arrFiles, (f) => { if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) { return fs.unlink(path.join(uplTempPath, f.filename)) } }) }) WIKI.logger.info('Purging orphaned upload files: [ COMPLETED ]') } catch (err) { WIKI.logger.error('Purging orphaned upload files: [ FAILED ]') WIKI.logger.error(err.message) } } ================================================ FILE: server/jobs/rebuild-tree.js ================================================ const _ = require('lodash') /* global WIKI */ module.exports = async (pageId) => { WIKI.logger.info(`Rebuilding page tree...`) try { WIKI.models = require('../core/db').init() await WIKI.configSvc.loadFromDb() await WIKI.configSvc.applyFlags() const pages = await WIKI.models.pages.query().select('id', 'path', 'localeCode', 'title', 'isPrivate', 'privateNS').orderBy(['localeCode', 'path']) let tree = [] let pik = 0 for (const page of pages) { const pagePaths = page.path.split('/') let currentPath = '' let depth = 0 let parentId = null let ancestors = [] for (const part of pagePaths) { depth++ const isFolder = (depth < pagePaths.length) currentPath = currentPath ? `${currentPath}/${part}` : part const found = _.find(tree, { localeCode: page.localeCode, path: currentPath }) if (!found) { pik++ tree.push({ id: pik, localeCode: page.localeCode, path: currentPath, depth: depth, title: isFolder ? part : page.title, isFolder: isFolder, isPrivate: !isFolder && page.isPrivate, privateNS: !isFolder ? page.privateNS : null, parent: parentId, pageId: isFolder ? null : page.id, ancestors: JSON.stringify(ancestors) }) parentId = pik } else if (isFolder && !found.isFolder) { found.isFolder = true parentId = found.id } else { parentId = found.id } ancestors.push(parentId) } } await WIKI.models.knex.table('pageTree').truncate() if (tree.length > 0) { // -> Save in chunks, because of per query max parameters (35k Postgres, 2k MSSQL, 1k for SQLite) if ((WIKI.config.db.type !== 'sqlite')) { for (const chunk of _.chunk(tree, 100)) { await WIKI.models.knex.table('pageTree').insert(chunk) } } else { for (const chunk of _.chunk(tree, 60)) { await WIKI.models.knex.table('pageTree').insert(chunk) } } } await WIKI.models.knex.destroy() WIKI.logger.info(`Rebuilding page tree: [ COMPLETED ]`) } catch (err) { WIKI.logger.error(`Rebuilding page tree: [ FAILED ]`) WIKI.logger.error(err.message) // exit process with error code throw err } } ================================================ FILE: server/jobs/render-page.js ================================================ const _ = require('lodash') const cheerio = require('cheerio') /* global WIKI */ module.exports = async (pageId) => { WIKI.logger.info(`Rendering page ID ${pageId}...`) try { WIKI.models = require('../core/db').init() await WIKI.configSvc.loadFromDb() await WIKI.configSvc.applyFlags() const page = await WIKI.models.pages.getPageFromDb(pageId) if (!page) { throw new Error('Invalid Page Id') } await WIKI.models.renderers.fetchDefinitions() const pipeline = await WIKI.models.renderers.getRenderingPipeline(page.contentType) let output = page.content if (_.isEmpty(page.content)) { await WIKI.models.knex.destroy() WIKI.logger.warn(`Failed to render page ID ${pageId} because content was empty: [ FAILED ]`) } for (let core of pipeline) { const renderer = require(`../modules/rendering/${_.kebabCase(core.key)}/renderer.js`) output = await renderer.render.call({ config: core.config, children: core.children, page: page, input: output }) } // Parse TOC const $ = cheerio.load(output) let isStrict = $('h1').length > 0 // <- Allows for documents using H2 as top level let toc = { root: [] } $('h1,h2,h3,h4,h5,h6').each((idx, el) => { const depth = _.toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2) let leafPathError = false const leafPath = _.reduce(_.times(depth), (curPath, curIdx) => { if (_.has(toc, curPath)) { const lastLeafIdx = _.get(toc, curPath).length - 1 if (lastLeafIdx >= 0) { curPath = `${curPath}[${lastLeafIdx}].children` } else { leafPathError = true } } return curPath }, 'root') if (leafPathError) { return } const leafSlug = $('.toc-anchor', el).first().attr('href') $('.toc-anchor', el).remove() _.get(toc, leafPath).push({ title: _.trim($(el).text()), anchor: leafSlug, children: [] }) }) // Save to DB await WIKI.models.pages.query() .patch({ render: output, toc: JSON.stringify(toc.root) }) .where('id', pageId) // Save to cache await WIKI.models.pages.savePageToCache({ ...page, render: output, toc: JSON.stringify(toc.root) }) await WIKI.models.knex.destroy() WIKI.logger.info(`Rendering page ID ${pageId}: [ COMPLETED ]`) } catch (err) { WIKI.logger.error(`Rendering page ID ${pageId}: [ FAILED ]`) WIKI.logger.error(err.message) // exit process with error code throw err } } ================================================ FILE: server/jobs/sanitize-svg.js ================================================ const fs = require('fs-extra') const { JSDOM } = require('jsdom') const createDOMPurify = require('dompurify') /* global WIKI */ module.exports = async (svgPath) => { WIKI.logger.info(`Sanitizing SVG file upload...`) try { let svgContents = await fs.readFile(svgPath, 'utf8') const window = new JSDOM('').window const DOMPurify = createDOMPurify(window) svgContents = DOMPurify.sanitize(svgContents) await fs.writeFile(svgPath, svgContents) WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`) } catch (err) { WIKI.logger.error(`Failed to sanitize SVG file upload: [ FAILED ]`) WIKI.logger.error(err.message) throw err } } ================================================ FILE: server/jobs/sync-graph-locales.js ================================================ const _ = require('lodash') const { createApolloFetch } = require('apollo-fetch') /* global WIKI */ module.exports = async () => { WIKI.logger.info('Syncing locales with Graph endpoint...') try { const apollo = createApolloFetch({ uri: WIKI.config.graphEndpoint }) // -> Fetch locales list const respList = await apollo({ query: `{ localization { locales { availability code name nativeName isRTL createdAt updatedAt } } }` }) const locales = _.sortBy(_.get(respList, 'data.localization.locales', []), 'name').map(lc => ({...lc, isInstalled: (lc.code === 'en')})) WIKI.cache.set('locales', locales) // -> Download locale strings if (WIKI.config.lang.autoUpdate) { const activeLocales = WIKI.config.lang.namespacing ? WIKI.config.lang.namespaces : [WIKI.config.lang.code] for (const currentLocale of activeLocales) { const localeInfo = _.find(locales, ['code', currentLocale]) const respStrings = await apollo({ query: `query ($code: String!) { localization { strings(code: $code) { key value } } }`, variables: { code: currentLocale } }) const strings = _.get(respStrings, 'data.localization.strings', []) let lcObj = {} _.forEach(strings, row => { if (_.includes(row.key, '::')) { return } if (_.isEmpty(row.value)) { row.value = row.key } _.set(lcObj, row.key.replace(':', '.'), row.value) }) await WIKI.models.locales.query().update({ code: currentLocale, strings: lcObj, isRTL: localeInfo.isRTL, name: localeInfo.name, nativeName: localeInfo.nativeName, availability: localeInfo.availability }).where('code', currentLocale) WIKI.logger.info(`Pulled latest locale updates for ${localeInfo.name} from Graph endpoint: [ COMPLETED ]`) } } await WIKI.lang.refreshNamespaces() WIKI.logger.info('Syncing locales with Graph endpoint: [ COMPLETED ]') } catch (err) { WIKI.logger.error('Syncing locales with Graph endpoint: [ FAILED ]') WIKI.logger.error(err.message) } } ================================================ FILE: server/jobs/sync-graph-updates.js ================================================ const _ = require('lodash') const { createApolloFetch } = require('apollo-fetch') /* global WIKI */ module.exports = async () => { WIKI.logger.info(`Fetching latest updates from Graph endpoint...`) try { const apollo = createApolloFetch({ uri: WIKI.config.graphEndpoint }) const resp = await apollo({ query: `query ($channel: ReleaseChannel!, $version: String!) { releases { checkForUpdates(channel: $channel, version: $version) { channel version releaseDate minimumVersionRequired minimumNodeRequired } } }`, variables: { channel: WIKI.config.channel, version: WIKI.version } }) const info = _.get(resp, 'data.releases.checkForUpdates', false) if (info) { WIKI.system.updates = info } WIKI.logger.info(`Fetching latest updates from Graph endpoint: [ COMPLETED ]`) } catch (err) { WIKI.logger.error(`Fetching latest updates from Graph endpoint: [ FAILED ]`) WIKI.logger.error(err.message) } } ================================================ FILE: server/jobs/sync-storage.js ================================================ const _ = require('lodash') /* global WIKI */ module.exports = async (targetKey) => { WIKI.logger.info(`Syncing with storage target ${targetKey}...`) try { const target = _.find(WIKI.models.storage.targets, ['key', targetKey]) if (target) { await target.fn.sync() WIKI.logger.info(`Syncing with storage target ${targetKey}: [ COMPLETED ]`) await WIKI.models.storage.query().patch({ state: { status: 'operational', message: '', lastAttempt: new Date().toISOString() } }).where('key', targetKey) } else { throw new Error('Invalid storage target. Unable to perform sync.') } } catch (err) { WIKI.logger.error(`Syncing with storage target ${targetKey}: [ FAILED ]`) WIKI.logger.error(err.message) await WIKI.models.storage.query().patch({ state: { status: 'error', message: err.message, lastAttempt: new Date().toISOString() } }).where('key', targetKey) } } ================================================ FILE: server/locales/README.md ================================================ ## IMPORTANT Localization files are not stored into files! Contact us on Gitter to request access to the translation web service: https://gitter.im/Requarks/wiki ## Development Mode If you need to add new keys and test them live, simply create a {LANG}.yml file in this folder containing the values you want to test. e.g.: ### en.yml ```yml admin: api.title: 'API Access' auth.title: 'Authentication' ``` The official localization keys will still be loaded first, but your local files will overwrite any existing keys (and add new ones). Note that you must restart Wiki.js to load any changes made to the files, which happens automatically on save when in dev mode. ================================================ FILE: server/master.js ================================================ const autoload = require('auto-load') const bodyParser = require('body-parser') const compression = require('compression') const cookieParser = require('cookie-parser') const cors = require('cors') const express = require('express') const session = require('express-session') const KnexSessionStore = require('connect-session-knex')(session) const favicon = require('serve-favicon') const path = require('path') const _ = require('lodash') /* global WIKI */ module.exports = async () => { // ---------------------------------------- // Load core modules // ---------------------------------------- WIKI.auth = require('./core/auth').init() WIKI.lang = require('./core/localization').init() WIKI.mail = require('./core/mail').init() WIKI.system = require('./core/system').init() // ---------------------------------------- // Load middlewares // ---------------------------------------- const mw = autoload(path.join(WIKI.SERVERPATH, '/middlewares')) const ctrl = autoload(path.join(WIKI.SERVERPATH, '/controllers')) // ---------------------------------------- // Define Express App // ---------------------------------------- const app = express() WIKI.app = app app.use(compression()) // ---------------------------------------- // Security // ---------------------------------------- app.use(mw.security) app.use(cors({ origin: false })) app.options('*', cors({ origin: false })) if (WIKI.config.security.securityTrustProxy) { app.enable('trust proxy') } // ---------------------------------------- // Public Assets // ---------------------------------------- app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico'))) app.use('/_assets/svg/twemoji', async (req, res, next) => { try { WIKI.asar.serve('twemoji', req, res, next) } catch (err) { res.sendStatus(404) } }) app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets'), { index: false, maxAge: '7d' })) // ---------------------------------------- // SSL Handlers // ---------------------------------------- app.use('/', ctrl.ssl) // ---------------------------------------- // Passport Authentication // ---------------------------------------- app.use(cookieParser()) app.use(session({ secret: WIKI.config.sessionSecret, resave: false, saveUninitialized: false, store: new KnexSessionStore({ knex: WIKI.models.knex }) })) app.use(WIKI.auth.passport.initialize()) app.use(WIKI.auth.authenticate) // ---------------------------------------- // GraphQL Server // ---------------------------------------- app.use(bodyParser.json({ limit: WIKI.config.bodyParserLimit || '1mb' })) await WIKI.servers.startGraphQL() // ---------------------------------------- // SEO // ---------------------------------------- app.use(mw.seo) // ---------------------------------------- // View Engine Setup // ---------------------------------------- app.set('views', path.join(WIKI.SERVERPATH, 'views')) app.set('view engine', 'pug') app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' })) // ---------------------------------------- // Localization // ---------------------------------------- WIKI.lang.attachMiddleware(app) // ---------------------------------------- // View accessible data // ---------------------------------------- app.locals.siteConfig = {} app.locals.analyticsCode = {} app.locals.basedir = WIKI.ROOTPATH app.locals.config = WIKI.config app.locals.pageMeta = { title: '', description: WIKI.config.description, image: '', url: '/' } app.locals.devMode = WIKI.devMode // ---------------------------------------- // HMR (Dev Mode Only) // ---------------------------------------- if (global.DEV) { app.use(global.WP_DEV.devMiddleware) app.use(global.WP_DEV.hotMiddleware) } // ---------------------------------------- // Routing // ---------------------------------------- app.use(async (req, res, next) => { res.locals.siteConfig = { title: WIKI.config.title, theme: WIKI.config.theming.theme, darkMode: WIKI.config.theming.darkMode, tocPosition: WIKI.config.theming.tocPosition || 'left', lang: WIKI.config.lang.code, rtl: WIKI.config.lang.rtl, company: WIKI.config.company, contentLicense: WIKI.config.contentLicense, footerOverride: WIKI.config.footerOverride, logoUrl: WIKI.config.logoUrl } res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true }) res.locals.analyticsCode = await WIKI.models.analytics.getCode({ cache: true }) next() }) app.use('/', ctrl.auth) app.use('/', ctrl.upload) app.use('/', ctrl.common) // ---------------------------------------- // Error handling // ---------------------------------------- app.use((req, res, next) => { const err = new Error('Not Found') err.status = 404 next(err) }) app.use((err, req, res, next) => { if (req.path === '/graphql') { res.status(err.status || 500).json({ data: {}, errors: [{ message: err.message, path: [] }] }) } else { res.status(err.status || 500) _.set(res.locals, 'pageMeta.title', 'Error') res.render('error', { message: err.message, error: WIKI.IS_DEBUG ? err : {} }) } }) // ---------------------------------------- // Start HTTP Server(s) // ---------------------------------------- await WIKI.servers.startHTTP() if (WIKI.config.ssl.enabled === true || WIKI.config.ssl.enabled === 'true' || WIKI.config.ssl.enabled === 1 || WIKI.config.ssl.enabled === '1') { await WIKI.servers.startHTTPS() } return true } ================================================ FILE: server/middlewares/security.js ================================================ /* global WIKI */ /** * Security Middleware * * @param {Express Request} req Express request object * @param {Express Response} res Express response object * @param {Function} next next callback function * @return {any} void */ module.exports = function (req, res, next) { // -> Disable X-Powered-By req.app.disable('x-powered-by') // -> Disable Frame Embedding if (WIKI.config.security.securityIframe) { res.set('X-Frame-Options', 'deny') } // -> Re-enable XSS Fitler if disabled res.set('X-XSS-Protection', '1; mode=block') // -> Disable MIME-sniffing res.set('X-Content-Type-Options', 'nosniff') // -> Disable IE Compatibility Mode res.set('X-UA-Compatible', 'IE=edge') // -> Disables referrer header when navigating to a different origin if (WIKI.config.security.securityReferrerPolicy) { res.set('Referrer-Policy', 'same-origin') } // -> Enforce HSTS if (WIKI.config.security.securityHSTS) { res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`) } // -> Prevent Open Redirect from user provided URL if (WIKI.config.security.securityOpenRedirect) { // Strips out all repeating / character in the provided URL req.url = req.url.replace(/(\/)(?=\/*\1)/g, '') } return next() } ================================================ FILE: server/middlewares/seo.js ================================================ const _ = require('lodash') /* global WIKI */ /** * SEO Middleware * * @param {Express Request} req Express request object * @param {Express Response} res Express response object * @param {Function} next next callback function * @return {any} void */ module.exports = function (req, res, next) { if (req.path.length > 1 && _.endsWith(req.path, '/')) { let query = req.url.slice(req.path.length) || '' res.redirect(301, req.path.slice(0, -1) + query) } else { _.set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`) return next() } } ================================================ FILE: server/models/analytics.js ================================================ const Model = require('objection').Model const fs = require('fs-extra') const path = require('path') const _ = require('lodash') const yaml = require('js-yaml') const commonHelper = require('../helpers/common') /* global WIKI */ /** * Analytics model */ module.exports = class Analytics extends Model { static get tableName() { return 'analytics' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key', 'isEnabled'], properties: { key: {type: 'string'}, isEnabled: {type: 'boolean'} } } } static get jsonAttributes() { return ['config'] } static async getProviders(isEnabled) { const providers = await WIKI.models.analytics.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {}) return _.sortBy(providers, ['key']) } static async refreshProvidersFromDisk() { let trx try { const dbProviders = await WIKI.models.analytics.query() // -> Fetch definitions from disk const analyticsDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/analytics')) let diskProviders = [] for (let dir of analyticsDirs) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', dir, 'definition.yml'), 'utf8') diskProviders.push(yaml.safeLoad(def)) } WIKI.data.analytics = diskProviders.map(provider => ({ ...provider, props: commonHelper.parseModuleProps(provider.props) })) let newProviders = [] for (let provider of WIKI.data.analytics) { if (!_.some(dbProviders, ['key', provider.key])) { newProviders.push({ key: provider.key, isEnabled: false, config: _.transform(provider.props, (result, value, key) => { _.set(result, key, value.default) return result }, {}) }) } else { const providerConfig = _.get(_.find(dbProviders, ['key', provider.key]), 'config', {}) await WIKI.models.analytics.query().patch({ config: _.transform(provider.props, (result, value, key) => { if (!_.has(result, key)) { _.set(result, key, value.default) } return result }, providerConfig) }).where('key', provider.key) } } if (newProviders.length > 0) { trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex) for (let provider of newProviders) { await WIKI.models.analytics.query(trx).insert(provider) } await trx.commit() WIKI.logger.info(`Loaded ${newProviders.length} new analytics providers: [ OK ]`) } else { WIKI.logger.info(`No new analytics providers found: [ SKIPPED ]`) } } catch (err) { WIKI.logger.error(`Failed to scan or load new analytics providers: [ FAILED ]`) WIKI.logger.error(err) if (trx) { trx.rollback() } } } static async getCode ({ cache = false } = {}) { if (cache) { const analyticsCached = await WIKI.cache.get('analytics') if (analyticsCached) { return analyticsCached } } try { const analyticsCode = { head: '', bodyStart: '', bodyEnd: '' } const providers = await WIKI.models.analytics.getProviders(true) for (let provider of providers) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', provider.key, 'code.yml'), 'utf8') let code = yaml.safeLoad(def) code.head = _.defaultTo(code.head, '') code.bodyStart = _.defaultTo(code.bodyStart, '') code.bodyEnd = _.defaultTo(code.bodyEnd, '') _.forOwn(provider.config, (value, key) => { code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value) code.bodyStart = _.replace(code.bodyStart, `{{${key}}}`, value) code.bodyEnd = _.replace(code.bodyEnd, `{{${key}}}`, value) }) analyticsCode.head += code.head analyticsCode.bodyStart += code.bodyStart analyticsCode.bodyEnd += code.bodyEnd } await WIKI.cache.set('analytics', analyticsCode, 300) return analyticsCode } catch (err) { WIKI.logger.warn('Error while getting analytics code: ', err) return { head: '', bodyStart: '', bodyEnd: '' } } } } ================================================ FILE: server/models/apiKeys.js ================================================ /* global WIKI */ const Model = require('objection').Model const moment = require('moment') const ms = require('ms') const jwt = require('jsonwebtoken') /** * Users model */ module.exports = class ApiKey extends Model { static get tableName() { return 'apiKeys' } static get jsonSchema () { return { type: 'object', required: ['name', 'key'], properties: { id: {type: 'integer'}, name: {type: 'string'}, key: {type: 'string'}, expiration: {type: 'string'}, isRevoked: {type: 'boolean'}, createdAt: {type: 'string'}, validUntil: {type: 'string'} } } } async $beforeUpdate(opt, context) { await super.$beforeUpdate(opt, context) this.updatedAt = moment.utc().toISOString() } async $beforeInsert(context) { await super.$beforeInsert(context) this.createdAt = moment.utc().toISOString() this.updatedAt = moment.utc().toISOString() } static async createNewKey ({ name, expiration, fullAccess, group }) { const entry = await WIKI.models.apiKeys.query().insert({ name, key: 'pending', expiration: moment.utc().add(ms(expiration), 'ms').toISOString(), isRevoked: true }) const key = jwt.sign({ api: entry.id, grp: fullAccess ? 1 : group }, { key: WIKI.config.certs.private, passphrase: WIKI.config.sessionSecret }, { algorithm: 'RS256', expiresIn: expiration, audience: WIKI.config.auth.audience, issuer: 'urn:wiki.js' }) await WIKI.models.apiKeys.query().findById(entry.id).patch({ key, isRevoked: false }) return key } } ================================================ FILE: server/models/assetFolders.js ================================================ const Model = require('objection').Model const _ = require('lodash') /* global WIKI */ /** * Users model */ module.exports = class AssetFolder extends Model { static get tableName() { return 'assetFolders' } static get jsonSchema () { return { type: 'object', properties: { id: {type: 'integer'}, name: {type: 'string'}, slug: {type: 'string'} } } } static get relationMappings() { return { parent: { relation: Model.BelongsToOneRelation, modelClass: AssetFolder, join: { from: 'assetFolders.folderId', to: 'assetFolders.id' } } } } /** * Get full folder hierarchy starting from specified folder to root * * @param {Number} folderId Id of the folder */ static async getHierarchy (folderId) { let hier if (WIKI.config.db.type === 'mssql') { hier = await WIKI.models.knex.with('ancestors', qb => { qb.select('id', 'name', 'slug', 'parentId').from('assetFolders').where('id', folderId).unionAll(sqb => { sqb.select('a.id', 'a.name', 'a.slug', 'a.parentId').from('assetFolders AS a').join('ancestors', 'ancestors.parentId', 'a.id') }) }).select('*').from('ancestors') } else { hier = await WIKI.models.knex.withRecursive('ancestors', qb => { qb.select('id', 'name', 'slug', 'parentId').from('assetFolders').where('id', folderId).union(sqb => { sqb.select('a.id', 'a.name', 'a.slug', 'a.parentId').from('assetFolders AS a').join('ancestors', 'ancestors.parentId', 'a.id') }) }).select('*').from('ancestors') } // The ancestors are from children to grandparents, must reverse for correct path order. return _.reverse(hier) } /** * Get full folder paths */ static async getAllPaths () { const all = await WIKI.models.assetFolders.query() let folders = {} all.forEach(fld => { _.set(folders, fld.id, fld.slug) let parentId = fld.parentId while (parentId !== null || parentId > 0) { const parent = _.find(all, ['id', parentId]) _.set(folders, fld.id, `${parent.slug}/${_.get(folders, fld.id)}`) parentId = parent.parentId } }) return folders } } ================================================ FILE: server/models/assets.js ================================================ /* global WIKI */ const Model = require('objection').Model const moment = require('moment') const path = require('path') const fs = require('fs-extra') const _ = require('lodash') const assetHelper = require('../helpers/asset') const Promise = require('bluebird') /** * Users model */ module.exports = class Asset extends Model { static get tableName() { return 'assets' } static get jsonSchema () { return { type: 'object', properties: { id: {type: 'integer'}, filename: {type: 'string'}, hash: {type: 'string'}, ext: {type: 'string'}, kind: {type: 'string'}, mime: {type: 'string'}, fileSize: {type: 'integer'}, metadata: {type: 'object'}, createdAt: {type: 'string'}, updatedAt: {type: 'string'} } } } static get relationMappings() { return { author: { relation: Model.BelongsToOneRelation, modelClass: require('./users'), join: { from: 'assets.authorId', to: 'users.id' } }, folder: { relation: Model.BelongsToOneRelation, modelClass: require('./assetFolders'), join: { from: 'assets.folderId', to: 'assetFolders.id' } } } } async $beforeUpdate(opt, context) { await super.$beforeUpdate(opt, context) this.updatedAt = moment.utc().toISOString() } async $beforeInsert(context) { await super.$beforeInsert(context) this.createdAt = moment.utc().toISOString() this.updatedAt = moment.utc().toISOString() } async getAssetPath() { let hierarchy = [] if (this.folderId) { hierarchy = await WIKI.models.assetFolders.getHierarchy(this.folderId) } return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename } async deleteAssetCache() { await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`)) } static async upload(opts) { const fileInfo = path.parse(opts.originalname) const fileHash = assetHelper.generateHash(opts.assetPath) // Check for existing asset let asset = await WIKI.models.assets.query().where({ hash: fileHash, folderId: opts.folderId }).first() // Build Object let assetRow = { filename: opts.originalname, hash: fileHash, ext: fileInfo.ext, kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary', mime: opts.mimetype, fileSize: opts.size, folderId: opts.folderId } // Sanitize SVG contents if ( WIKI.config.uploads.scanSVG && ( opts.mimetype.toLowerCase().startsWith('image/svg') || fileInfo.ext.toLowerCase() === '.svg' ) ) { const svgSanitizeJob = await WIKI.scheduler.registerJob({ name: 'sanitize-svg', immediate: true, worker: true }, opts.path) await svgSanitizeJob.finished } // Save asset data try { const fileBuffer = await fs.readFile(opts.path) if (asset) { // Patch existing asset if (opts.mode === 'upload') { assetRow.authorId = opts.user.id } await WIKI.models.assets.query().patch(assetRow).findById(asset.id) await WIKI.models.knex('assetData').where({ id: asset.id }).update({ data: fileBuffer }) } else { // Create asset entry assetRow.authorId = opts.user.id asset = await WIKI.models.assets.query().insert(assetRow) await WIKI.models.knex('assetData').insert({ id: asset.id, data: fileBuffer }) } // Move temp upload to cache if (opts.mode === 'upload') { await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true }) } else { await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true }) } // Add to Storage if (!opts.skipStorage) { await WIKI.models.storage.assetEvent({ event: 'uploaded', asset: { ...asset, path: await asset.getAssetPath(), data: fileBuffer, authorId: opts.user.id, authorName: opts.user.name, authorEmail: opts.user.email } }) } } catch (err) { WIKI.logger.warn(err) } } static async getAsset(assetPath, res) { try { const fileInfo = assetHelper.getPathInfo(assetPath) const fileHash = assetHelper.generateHash(assetPath) const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`) // Force unsafe extensions to download if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) { res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base)) } if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) { return } if (await WIKI.models.assets.getAssetFromStorage(assetPath, res)) { return } await WIKI.models.assets.getAssetFromDb(assetPath, fileHash, cachePath, res) } catch (err) { if (err.code === `ECONNABORTED` || err.code === `EPIPE`) { return } WIKI.logger.error(err) res.sendStatus(500) } } static async getAssetFromCache(assetPath, cachePath, res) { try { await fs.access(cachePath, fs.constants.R_OK) } catch (err) { return false } const sendFile = Promise.promisify(res.sendFile, {context: res}) res.type(path.extname(assetPath)) await sendFile(cachePath, { dotfiles: 'deny' }) return true } static async getAssetFromStorage(assetPath, res) { const localLocations = await WIKI.models.storage.getLocalLocations({ asset: { path: assetPath } }) for (let location of _.filter(localLocations, location => Boolean(location.path))) { const assetExists = await WIKI.models.assets.getAssetFromCache(assetPath, location.path, res) if (assetExists) { return true } } return false } static async getAssetFromDb(assetPath, fileHash, cachePath, res) { const asset = await WIKI.models.assets.query().where('hash', fileHash).first() if (asset) { const assetData = await WIKI.models.knex('assetData').where('id', asset.id).first() res.type(asset.ext) res.send(assetData.data) await fs.outputFile(cachePath, assetData.data) } else { res.sendStatus(404) } } static async flushTempUploads() { return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`)) } } ================================================ FILE: server/models/authentication.js ================================================ const Model = require('objection').Model const fs = require('fs-extra') const path = require('path') const _ = require('lodash') const yaml = require('js-yaml') const commonHelper = require('../helpers/common') /* global WIKI */ /** * Authentication model */ module.exports = class Authentication extends Model { static get tableName() { return 'authentication' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key'], properties: { key: {type: 'string'}, selfRegistration: {type: 'boolean'} } } } static get jsonAttributes() { return ['config', 'domainWhitelist', 'autoEnrollGroups'] } static async getStrategy(key) { return WIKI.models.authentication.query().findOne({ key }) } static async getStrategies() { const strategies = await WIKI.models.authentication.query().orderBy('order') return strategies.map(str => ({ ...str, domainWhitelist: _.get(str.domainWhitelist, 'v', []), autoEnrollGroups: _.get(str.autoEnrollGroups, 'v', []) })) } static async getStrategiesForLegacyClient() { const strategies = await WIKI.models.authentication.query().select('key', 'selfRegistration') let formStrategies = [] let socialStrategies = [] for (let stg of strategies) { const stgInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {} if (stgInfo.useForm) { formStrategies.push({ key: stg.key, title: stgInfo.title }) } else { socialStrategies.push({ ...stgInfo, ...stg, icon: await fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${stg.key}.svg`), 'utf8').catch(err => { if (err.code === 'ENOENT') { return null } throw err }) }) } } return { formStrategies, socialStrategies } } static async refreshStrategiesFromDisk() { try { const dbStrategies = await WIKI.models.authentication.query() // -> Fetch definitions from disk const authDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/authentication')) WIKI.data.authentication = [] for (let dir of authDirs) { const defRaw = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/authentication', dir, 'definition.yml'), 'utf8') const def = yaml.safeLoad(defRaw) WIKI.data.authentication.push({ ...def, props: commonHelper.parseModuleProps(def.props) }) } for (const strategy of dbStrategies) { let newProps = false const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey]) if (!strategyDef) { await WIKI.models.authentication.query().delete().where('key', strategy.key) WIKI.logger.info(`Authentication strategy ${strategy.strategyKey} was removed from disk: [ REMOVED ]`) continue } strategy.config = _.transform(strategyDef.props, (result, value, key) => { if (!_.has(result, key)) { _.set(result, key, value.default) // we have some new properties added to an existing auth strategy to write to the database newProps = true } return result }, strategy.config) // Fix pre-2.5 strategies displayName if (!strategy.displayName) { await WIKI.models.authentication.query().patch({ displayName: strategyDef.title }).where('key', strategy.key) } // write existing auth model to database with new properties and defaults if (newProps) { await WIKI.models.authentication.query().patch({ config: strategy.config }).where('key', strategy.key) } } WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`) } catch (err) { WIKI.logger.error(`Failed to scan or load new authentication providers: [ FAILED ]`) WIKI.logger.error(err) } } } ================================================ FILE: server/models/commentProviders.js ================================================ const Model = require('objection').Model const fs = require('fs-extra') const path = require('path') const _ = require('lodash') const yaml = require('js-yaml') const commonHelper = require('../helpers/common') /* global WIKI */ /** * CommentProvider model */ module.exports = class CommentProvider extends Model { static get tableName() { return 'commentProviders' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key', 'isEnabled'], properties: { key: {type: 'string'}, isEnabled: {type: 'boolean'} } } } static get jsonAttributes() { return ['config'] } static async getProvider(key) { return WIKI.models.commentProviders.query().findOne({ key }) } static async getProviders(isEnabled) { const providers = await WIKI.models.commentProviders.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {}) return _.sortBy(providers, ['key']) } static async refreshProvidersFromDisk() { let trx try { const dbProviders = await WIKI.models.commentProviders.query() // -> Fetch definitions from disk const commentDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/comments')) let diskProviders = [] for (let dir of commentDirs) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', dir, 'definition.yml'), 'utf8') diskProviders.push(yaml.safeLoad(def)) } WIKI.data.commentProviders = diskProviders.map(provider => ({ ...provider, props: commonHelper.parseModuleProps(provider.props) })) let newProviders = [] for (let provider of WIKI.data.commentProviders) { if (!_.some(dbProviders, ['key', provider.key])) { newProviders.push({ key: provider.key, isEnabled: provider.key === 'default', config: _.transform(provider.props, (result, value, key) => { _.set(result, key, value.default) return result }, {}) }) } else { const providerConfig = _.get(_.find(dbProviders, ['key', provider.key]), 'config', {}) await WIKI.models.commentProviders.query().patch({ config: _.transform(provider.props, (result, value, key) => { if (!_.has(result, key)) { _.set(result, key, value.default) } return result }, providerConfig) }).where('key', provider.key) } } if (newProviders.length > 0) { trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex) for (let provider of newProviders) { await WIKI.models.commentProviders.query(trx).insert(provider) } await trx.commit() WIKI.logger.info(`Loaded ${newProviders.length} new comment providers: [ OK ]`) } else { WIKI.logger.info(`No new comment providers found: [ SKIPPED ]`) } } catch (err) { WIKI.logger.error(`Failed to scan or load new comment providers: [ FAILED ]`) WIKI.logger.error(err) if (trx) { trx.rollback() } } } static async initProvider() { const commentProvider = await WIKI.models.commentProviders.query().findOne('isEnabled', true) if (commentProvider) { WIKI.data.commentProvider = { ..._.find(WIKI.data.commentProviders, ['key', commentProvider.key]), head: '', bodyStart: '', bodyEnd: '', main: '' } if (WIKI.data.commentProvider.codeTemplate) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', commentProvider.key, 'code.yml'), 'utf8') let code = yaml.safeLoad(def) code.head = _.defaultTo(code.head, '') code.body = _.defaultTo(code.body, '') code.main = _.defaultTo(code.main, '') _.forOwn(commentProvider.config, (value, key) => { code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value) code.body = _.replace(code.body, new RegExp(`{{${key}}}`, 'g'), value) code.main = _.replace(code.main, new RegExp(`{{${key}}}`, 'g'), value) }) WIKI.data.commentProvider.head = code.head WIKI.data.commentProvider.body = code.body WIKI.data.commentProvider.main = code.main } else { WIKI.data.commentProvider = { ...WIKI.data.commentProvider, ...require(`../modules/comments/${commentProvider.key}/comment`), config: commentProvider.config } await WIKI.data.commentProvider.init() } WIKI.data.commentProvider.config = commentProvider.config } } } ================================================ FILE: server/models/comments.js ================================================ const Model = require('objection').Model const validate = require('validate.js') const _ = require('lodash') /* global WIKI */ /** * Comments model */ module.exports = class Comment extends Model { static get tableName() { return 'comments' } static get jsonSchema () { return { type: 'object', required: [], properties: { id: {type: 'integer'}, content: {type: 'string'}, render: {type: 'string'}, name: {type: 'string'}, email: {type: 'string'}, ip: {type: 'string'}, createdAt: {type: 'string'}, updatedAt: {type: 'string'} } } } static get relationMappings() { return { author: { relation: Model.BelongsToOneRelation, modelClass: require('./users'), join: { from: 'comments.authorId', to: 'users.id' } }, page: { relation: Model.BelongsToOneRelation, modelClass: require('./pages'), join: { from: 'comments.pageId', to: 'pages.id' } } } } $beforeUpdate() { this.updatedAt = new Date().toISOString() } $beforeInsert() { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() } /** * Post New Comment */ static async postNewComment ({ pageId, replyTo, content, guestName, guestEmail, user, ip }) { // -> Input validation if (user.id === 2) { const validation = validate({ email: _.toLower(guestEmail), name: guestName }, { email: { email: true, length: { maximum: 255 } }, name: { presence: { allowEmpty: false }, length: { minimum: 2, maximum: 255 } } }, { format: 'flat' }) if (validation && validation.length > 0) { throw new WIKI.Error.InputInvalid(validation[0]) } } content = _.trim(content) if (content.length < 2) { throw new WIKI.Error.CommentContentMissing() } // -> Load Page const page = await WIKI.models.pages.getPageFromDb(pageId) if (page) { if (!WIKI.auth.checkAccess(user, ['write:comments'], { path: page.path, locale: page.localeCode, tags: page.tags })) { throw new WIKI.Error.CommentPostForbidden() } } else { throw new WIKI.Error.PageNotFound() } // -> Process by comment provider return WIKI.data.commentProvider.create({ page, replyTo, content, user: { ...user, ...(user.id === 2) ? { name: guestName, email: guestEmail } : {}, ip } }) } /** * Update an Existing Comment */ static async updateComment ({ id, content, user, ip }) { // -> Load Page const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id) if (!pageId) { throw new WIKI.Error.CommentNotFound() } const page = await WIKI.models.pages.getPageFromDb(pageId) if (page) { if (!WIKI.auth.checkAccess(user, ['manage:comments'], { path: page.path, locale: page.localeCode, tags: page.tags })) { throw new WIKI.Error.CommentManageForbidden() } } else { throw new WIKI.Error.PageNotFound() } // -> Process by comment provider return WIKI.data.commentProvider.update({ id, content, page, user: { ...user, ip } }) } /** * Delete an Existing Comment */ static async deleteComment ({ id, user, ip }) { // -> Load Page const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id) if (!pageId) { throw new WIKI.Error.CommentNotFound() } const page = await WIKI.models.pages.getPageFromDb(pageId) if (page) { if (!WIKI.auth.checkAccess(user, ['manage:comments'], { path: page.path, locale: page.localeCode, tags: page.tags })) { throw new WIKI.Error.CommentManageForbidden() } } else { throw new WIKI.Error.PageNotFound() } // -> Process by comment provider await WIKI.data.commentProvider.remove({ id, page, user: { ...user, ip } }) } } ================================================ FILE: server/models/editors.js ================================================ const Model = require('objection').Model const fs = require('fs-extra') const path = require('path') const _ = require('lodash') const yaml = require('js-yaml') const commonHelper = require('../helpers/common') /* global WIKI */ /** * Editor model */ module.exports = class Editor extends Model { static get tableName() { return 'editors' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key', 'isEnabled'], properties: { key: {type: 'string'}, isEnabled: {type: 'boolean'} } } } static get jsonAttributes() { return ['config'] } static async getEditors() { return WIKI.models.editors.query() } static async refreshEditorsFromDisk() { let trx try { const dbEditors = await WIKI.models.editors.query() // -> Fetch definitions from disk const editorDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/editor')) let diskEditors = [] for (let dir of editorDirs) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/editor', dir, 'definition.yml'), 'utf8') diskEditors.push(yaml.safeLoad(def)) } WIKI.data.editors = diskEditors.map(editor => ({ ...editor, props: commonHelper.parseModuleProps(editor.props) })) // -> Insert new editors let newEditors = [] for (let editor of WIKI.data.editors) { if (!_.some(dbEditors, ['key', editor.key])) { newEditors.push({ key: editor.key, isEnabled: false, config: _.transform(editor.props, (result, value, key) => { _.set(result, key, value.default) return result }, {}) }) } else { const editorConfig = _.get(_.find(dbEditors, ['key', editor.key]), 'config', {}) await WIKI.models.editors.query().patch({ config: _.transform(editor.props, (result, value, key) => { if (!_.has(result, key)) { _.set(result, key, value.default) } return result }, editorConfig) }).where('key', editor.key) } } if (newEditors.length > 0) { trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex) for (let editor of newEditors) { await WIKI.models.editors.query(trx).insert(editor) } await trx.commit() WIKI.logger.info(`Loaded ${newEditors.length} new editors: [ OK ]`) } else { WIKI.logger.info(`No new editors found: [ SKIPPED ]`) } } catch (err) { WIKI.logger.error(`Failed to scan or load new editors: [ FAILED ]`) WIKI.logger.error(err) if (trx) { trx.rollback() } } } static async getDefaultEditor(contentType) { // TODO - hardcoded for now switch (contentType) { case 'markdown': return 'markdown' case 'html': return 'ckeditor' case 'asciidoc': return 'asciidoc' default: return 'code' } } } ================================================ FILE: server/models/groups.js ================================================ const Model = require('objection').Model /** * Groups model */ module.exports = class Group extends Model { static get tableName() { return 'groups' } static get jsonSchema () { return { type: 'object', required: ['name'], properties: { id: {type: 'integer'}, name: {type: 'string'}, isSystem: {type: 'boolean'}, redirectOnLogin: {type: 'string'}, createdAt: {type: 'string'}, updatedAt: {type: 'string'} } } } static get jsonAttributes() { return ['permissions', 'pageRules'] } static get relationMappings() { return { users: { relation: Model.ManyToManyRelation, modelClass: require('./users'), join: { from: 'groups.id', through: { from: 'userGroups.groupId', to: 'userGroups.userId' }, to: 'users.id' } } } } $beforeUpdate() { this.updatedAt = new Date().toISOString() } $beforeInsert() { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() } } ================================================ FILE: server/models/locales.js ================================================ const Model = require('objection').Model /* global WIKI */ /** * Locales model */ module.exports = class Locale extends Model { static get tableName() { return 'locales' } static get idColumn() { return 'code' } static get jsonSchema () { return { type: 'object', required: ['code', 'name'], properties: { code: {type: 'string'}, isRTL: {type: 'boolean', default: false}, name: {type: 'string'}, nativeName: {type: 'string'}, createdAt: {type: 'string'}, updatedAt: {type: 'string'}, availability: {type: 'integer'} } } } static get jsonAttributes() { return ['strings'] } $beforeUpdate() { this.updatedAt = new Date().toISOString() } $beforeInsert() { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() } static async getNavLocales({ cache = false } = {}) { if (!WIKI.config.lang.namespacing) { return [] } if (cache) { const navLocalesCached = await WIKI.cache.get('nav:locales') if (navLocalesCached) { return navLocalesCached } } const navLocales = await WIKI.models.locales.query().select('code', 'nativeName AS name').whereIn('code', WIKI.config.lang.namespaces).orderBy('code') if (navLocales) { if (cache) { await WIKI.cache.set('nav:locales', navLocales, 300) } return navLocales } else { WIKI.logger.warn('Site Locales for navigation are missing or corrupted.') return [] } } } ================================================ FILE: server/models/loggers.js ================================================ const Model = require('objection').Model const path = require('path') const fs = require('fs-extra') const _ = require('lodash') const yaml = require('js-yaml') const commonHelper = require('../helpers/common') /* global WIKI */ /** * Logger model */ module.exports = class Logger extends Model { static get tableName() { return 'loggers' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key', 'isEnabled'], properties: { key: {type: 'string'}, isEnabled: {type: 'boolean'}, level: {type: 'string'} } } } static get jsonAttributes() { return ['config'] } static async getLoggers() { return WIKI.models.loggers.query() } static async refreshLoggersFromDisk() { let trx try { const dbLoggers = await WIKI.models.loggers.query() // -> Fetch definitions from disk const loggersDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/logging')) let diskLoggers = [] for (let dir of loggersDirs) { const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/logging', dir, 'definition.yml'), 'utf8') diskLoggers.push(yaml.safeLoad(def)) } WIKI.data.loggers = diskLoggers.map(logger => ({ ...logger, props: commonHelper.parseModuleProps(logger.props) })) // -> Insert new loggers let newLoggers = [] for (let logger of WIKI.data.loggers) { if (!_.some(dbLoggers, ['key', logger.key])) { newLoggers.push({ key: logger.key, isEnabled: (logger.key === 'console'), level: logger.defaultLevel, config: _.transform(logger.props, (result, value, key) => { _.set(result, key, value.default) return result }, {}) }) } else { const loggerConfig = _.get(_.find(dbLoggers, ['key', logger.key]), 'config', {}) await WIKI.models.loggers.query().patch({ config: _.transform(logger.props, (result, value, key) => { if (!_.has(result, key)) { _.set(result, key, value.default) } return result }, loggerConfig) }).where('key', logger.key) } } if (newLoggers.length > 0) { trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex) for (let logger of newLoggers) { await WIKI.models.loggers.query(trx).insert(logger) } await trx.commit() WIKI.logger.info(`Loaded ${newLoggers.length} new loggers: [ OK ]`) } else { WIKI.logger.info(`No new loggers found: [ SKIPPED ]`) } } catch (err) { WIKI.logger.error(`Failed to scan or load new loggers: [ FAILED ]`) WIKI.logger.error(err) if (trx) { trx.rollback() } } } static async pageEvent({ event, page }) { const loggers = await WIKI.models.storage.query().where('isEnabled', true) if (loggers && loggers.length > 0) { _.forEach(loggers, logger => { WIKI.queue.job.syncStorage.add({ event, logger, page }, { removeOnComplete: true }) }) } } } ================================================ FILE: server/models/navigation.js ================================================ const Model = require('objection').Model const _ = require('lodash') /* global WIKI */ /** * Navigation model */ module.exports = class Navigation extends Model { static get tableName() { return 'navigation' } static get idColumn() { return 'key' } static get jsonSchema () { return { type: 'object', required: ['key'], properties: { key: {type: 'string'}, config: {type: 'array', items: {type: 'object'}} } } } static async getTree({ cache = false, locale = 'en', groups = [], bypassAuth = false } = {}) { if (cache) { const navTreeCached = await WIKI.cache.get(`nav:sidebar:${locale}`) if (navTreeCached) { return bypassAuth ? navTreeCached : WIKI.models.navigation.getAuthorizedItems(navTreeCached, groups) } } const navTree = await WIKI.models.navigation.query().findOne('key', `site`) if (navTree) { // Check for pre-2.3 format if (_.has(navTree.config[0], 'kind')) { navTree.config = [{ locale: 'en', items: navTree.config.map(item => ({ ...item, visibilityMode: 'all', visibilityGroups: [] })) }] } for (const tree of navTree.config) { if (cache) { await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300) } } if (bypassAuth) { return locale === 'all' ? navTree.config : WIKI.cache.get(`nav:sidebar:${locale}`) } else { return locale === 'all' ? WIKI.models.navigation.getAuthorizedItems(navTree.config, groups) : WIKI.models.navigation.getAuthorizedItems(WIKI.cache.get(`nav:sidebar:${locale}`), groups) } } else { WIKI.logger.warn('Site Navigation is missing or corrupted.') return [] } } static getAuthorizedItems(tree = [], groups = []) { return _.filter(tree, leaf => { return leaf.visibilityMode === 'all' || _.intersection(leaf.visibilityGroups, groups).length > 0 }) } } ================================================ FILE: server/models/pageHistory.js ================================================ const Model = require('objection').Model const _ = require('lodash') const { DateTime, Duration } = require('luxon') /* global WIKI */ /** * Page History model */ module.exports = class PageHistory extends Model { static get tableName() { return 'pageHistory' } static get jsonSchema () { return { type: 'object', required: ['path', 'title'], properties: { id: {type: 'integer'}, path: {type: 'string'}, hash: {type: 'string'}, title: {type: 'string'}, description: {type: 'string'}, isPublished: {type: 'boolean'}, publishStartDate: {type: 'string'}, publishEndDate: {type: 'string'}, content: {type: 'string'}, contentType: {type: 'string'}, createdAt: {type: 'string'} } } } static get relationMappings() { return { tags: { relation: Model.ManyToManyRelation, modelClass: require('./tags'), join: { from: 'pageHistory.id', through: { from: 'pageHistoryTags.pageId', to: 'pageHistoryTags.tagId' }, to: 'tags.id' } }, page: { relation: Model.BelongsToOneRelation, modelClass: require('./pages'), join: { from: 'pageHistory.pageId', to: 'pages.id' } }, author: { relation: Model.BelongsToOneRelation, modelClass: require('./users'), join: { from: 'pageHistory.authorId', to: 'users.id' } }, editor: { relation: Model.BelongsToOneRelation, modelClass: require('./editors'), join: { from: 'pageHistory.editorKey', to: 'editors.key' } }, locale: { relation: Model.BelongsToOneRelation, modelClass: require('./locales'), join: { from: 'pageHistory.localeCode', to: 'locales.code' } } } } $beforeInsert() { this.createdAt = new Date().toISOString() } /** * Create Page Version */ static async addVersion(opts) { await WIKI.models.pageHistory.query().insert({ pageId: opts.id, authorId: opts.authorId, content: opts.content, contentType: opts.contentType, description: opts.description, editorKey: opts.editorKey, hash: opts.hash, isPrivate: (opts.isPrivate === true || opts.isPrivate === 1), isPublished: (opts.isPublished === true || opts.isPublished === 1), localeCode: opts.localeCode, path: opts.path, publishEndDate: opts.publishEndDate || '', publishStartDate: opts.publishStartDate || '', title: opts.title, action: opts.action || 'updated', versionDate: opts.versionDate }) } /** * Get Page Version */ static async getVersion({ pageId, versionId }) { const version = await WIKI.models.pageHistory.query() .column([ 'pageHistory.path', 'pageHistory.title', 'pageHistory.description', 'pageHistory.isPrivate', 'pageHistory.isPublished', 'pageHistory.publishStartDate', 'pageHistory.publishEndDate', 'pageHistory.content', 'pageHistory.contentType', 'pageHistory.createdAt', 'pageHistory.action', 'pageHistory.authorId', 'pageHistory.pageId', 'pageHistory.versionDate', { versionId: 'pageHistory.id', editor: 'pageHistory.editorKey', locale: 'pageHistory.localeCode', authorName: 'author.name' } ]) .joinRelated('author') .where({ 'pageHistory.id': versionId, 'pageHistory.pageId': pageId }).first() if (version) { return { ...version, updatedAt: version.createdAt || null, tags: [] } } else { return null } } /** * Get History Trail of a Page */ static async getHistory({ pageId, offsetPage = 0, offsetSize = 100 }) { const history = await WIKI.models.pageHistory.query() .column([ 'pageHistory.id', 'pageHistory.path', 'pageHistory.authorId', 'pageHistory.action', 'pageHistory.versionDate', { authorName: 'author.name' } ]) .joinRelated('author') .where({ 'pageHistory.pageId': pageId }) .orderBy('pageHistory.versionDate', 'desc') .page(offsetPage, offsetSize) let prevPh = null const upperLimit = (offsetPage + 1) * offsetSize if (history.total >= upperLimit) { prevPh = await WIKI.models.pageHistory.query() .column([ 'pageHistory.id', 'pageHistory.path', 'pageHistory.authorId', 'pageHistory.action', 'pageHistory.versionDate', { authorName: 'author.name' } ]) .joinRelated('author') .where({ 'pageHistory.pageId': pageId }) .orderBy('pageHistory.versionDate', 'desc') .offset((offsetPage + 1) * offsetSize) .limit(1) .first() } return { trail: _.reduce(_.reverse(history.results), (res, ph) => { let actionType = 'edit' let valueBefore = null let valueAfter = null if (!prevPh && history.total < upperLimit) { actionType = 'initial' } else if (_.get(prevPh, 'path', '') !== ph.path) { actionType = 'move' valueBefore = _.get(prevPh, 'path', '') valueAfter = ph.path } res.unshift({ versionId: ph.id, authorId: ph.authorId, authorName: ph.authorName, actionType, valueBefore, valueAfter, versionDate: ph.versionDate }) prevPh = ph return res }, []), total: history.total } } /** * Purge history older than X * * @param {String} olderThan ISO 8601 Duration */ static async purge (olderThan) { const dur = Duration.fromISO(olderThan) const olderThanISO = DateTime.utc().minus(dur) await WIKI.models.pageHistory.query().where('versionDate', '<', olderThanISO.toISO()).del() } } ================================================ FILE: server/models/pageLinks.js ================================================ const Model = require('objection').Model /** * Users model */ module.exports = class PageLink extends Model { static get tableName() { return 'pageLinks' } static get jsonSchema () { return { type: 'object', required: ['path', 'localeCode'], properties: { id: {type: 'integer'}, path: {type: 'string'}, localeCode: {type: 'string'} } } } static get relationMappings() { return { page: { relation: Model.BelongsToOneRelation, modelClass: require('./pages'), join: { from: 'pageLinks.pageId', to: 'pages.id' } } } } } ================================================ FILE: server/models/pages.js ================================================ const Model = require('objection').Model const _ = require('lodash') const JSBinType = require('js-binary').Type const pageHelper = require('../helpers/page') const path = require('path') const fs = require('fs-extra') const yaml = require('js-yaml') const striptags = require('striptags') const emojiRegex = require('emoji-regex') const he = require('he') const CleanCSS = require('clean-css') const TurndownService = require('turndown') const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm const cheerio = require('cheerio') /* global WIKI */ const frontmatterRegex = { html: /^()?(?:\n|\r)*([\w\W]*)*/, legacy: /^( ================================================ FILE: server/modules/analytics/baidutongji/definition.yml ================================================ key: baidutongji title: Baidu Tongji description: Baidu Tongji is a web analytics service offered by Baidu that tracks and reports website traffic. author: lawrenceching logo: https://static.requarks.io/logo/baidu.svg website: https://tongji.baidu.com isAvailable: true props: propertyTrackingId: type: String title: Property Tracking ID hint: Unique Property ID (found at the end of the tracking URL, e.g. https://hm.baidu.com/hm.js?XXXXXXXXXXXX) order: 1 ================================================ FILE: server/modules/analytics/countly/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/countly/definition.yml ================================================ key: countly title: Countly description: Countly is the best analytics platform to understand and enhance customer journeys in web, desktop and mobile applications. author: requarks.io logo: https://static.requarks.io/logo/countly.svg website: https://count.ly/ isAvailable: true props: appKey: type: String title: App Key hint: The App Key found under Management > Applications order: 1 serverUrl: type: String title: Server URL hint: The Count.ly server to report to. e.g. https://us-example.count.ly order: 2 ================================================ FILE: server/modules/analytics/elasticapm/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/elasticapm/definition.yml ================================================ key: elasticapm title: Elasticsearch APM RUM description: Real User Monitoring captures user interaction with clients such as web browsers. author: requarks.io logo: https://static.requarks.io/logo/elasticsearch-apm.svg website: https://www.elastic.co/solutions/apm isAvailable: true props: serverUrl: type: String title: APM Server URL hint: The full URL to your APM server, including the port default: http://apm.example.com:8200 order: 1 serviceName: type: String title: Service Name hint: The name of the client reported to APM default: wiki-js order: 2 environment: type: String title: Environment hint: e.g. production/development/test default: '' order: 3 ================================================ FILE: server/modules/analytics/fathom/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/fathom/definition.yml ================================================ key: fathom title: Fathom description: Fathom Analytics provides simple, useful website stats without tracking or storing personal data of your users. author: requarks.io logo: https://static.requarks.io/logo/fathom.svg website: https://usefathom.com/ isAvailable: true props: host: type: String title: Fathom Server Host hint: The hostname / ip adress where Fathom is installed, without the trailing slash. e.g. https://fathom.example.com order: 1 siteId: type: String title: Site ID hint: The alphanumeric identifier of your site order: 2 ================================================ FILE: server/modules/analytics/fullstory/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/fullstory/definition.yml ================================================ key: fullstory title: FullStory description: FullStory is your digital experience analytics platform for on-the-fly funnels, pixel-perfect replay, custom events, heat maps, advanced search, Dev Tools, and more. author: requarks.io logo: https://static.requarks.io/logo/fullstory.svg website: https://www.fullstory.com isAvailable: true props: org: type: String title: Organization ID hint: A 5 alphanumeric identifier, e.g. XXXXX order: 1 ================================================ FILE: server/modules/analytics/google/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/google/definition.yml ================================================ key: google title: Google Analytics description: Google Analytics is a web analytics service offered by Google that tracks and reports website traffic. author: requarks.io logo: https://static.requarks.io/logo/google-analytics.svg website: https://analytics.google.com/ isAvailable: true props: propertyTrackingId: type: String title: Property Tracking ID hint: G-XXXXXXXXXX order: 1 ================================================ FILE: server/modules/analytics/gtm/code.yml ================================================ head: | bodyStart: | ================================================ FILE: server/modules/analytics/gtm/definition.yml ================================================ key: gtm title: Google Tag Manager description: Google Tag Manager is a tag management system created by Google to manage JavaScript and HTML tags used for tracking and analytics on websites. author: requarks.io logo: https://static.requarks.io/logo/google-tag-manager.svg website: https://tagmanager.google.com isAvailable: true props: containerTrackingId: type: String title: Container Tracking ID hint: GTM-XXXXXXX order: 1 ================================================ FILE: server/modules/analytics/hotjar/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/hotjar/definition.yml ================================================ key: hotjar title: Hotjar description: Hotjar is the fast & visual way to understand your users, providing everything your team needs to uncover insights and make the right changes to your site. author: requarks.io logo: https://static.requarks.io/logo/hotjar.svg website: https://www.hotjar.com isAvailable: true props: siteId: type: String title: Site ID hint: A numeric identifier of your site order: 1 ================================================ FILE: server/modules/analytics/matomo/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/matomo/definition.yml ================================================ key: matomo title: Matomo description: Take back control with Matomo Analytics – a powerful web analytics platform that gives you and your business 100% data ownership and user privacy protection. author: requarks.io logo: https://static.requarks.io/logo/matomo.svg website: https://matomo.org/ isAvailable: true props: siteId: type: String title: Site ID hint: The number index representing your site ID default: 1 order: 1 serverHost: type: String title: Server Host hint: Including https:// and optionally the port. Without trailing slash. (e.g. https://example.matomo.cloud) default: https://example.matomo.cloud order: 2 ================================================ FILE: server/modules/analytics/newrelic/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/newrelic/definition.yml ================================================ key: newrelic title: New Relic Browser description: New Relic Browser provides deep visibility and insight into how your users are interacting with your application or website. author: requarks.io logo: https://static.requarks.io/logo/newrelic.svg website: https://newrelic.com/products/browser-monitoring isAvailable: true props: licenseKey: type: String title: License Key hint: Found at the very end of the code snippet provided by New Relic Browser order: 1 appId: type: String title: Application ID hint: Found at the very end of the code snippet provided by New Relic Browser order: 2 beacon: type: String title: Beacon default: bam.nr-data.net hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers. order: 3 errorBeacon: type: String title: Error Beacon default: bam.nr-data.net hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers. order: 4 ================================================ FILE: server/modules/analytics/plausible/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/plausible/definition.yml ================================================ key: plausible title: Plausible Analytics description: Simple, open-source, lightweight and privacy-friendly web analytics alternative to Google Analytics. author: requarks.io logo: https://cdn.js.wiki/images/3rdparty/plausible.svg website: https://plausible.io isAvailable: true props: domain: type: String title: Domain hint: The value of the data-domain property order: 1 plausibleJsSrc: type: String default: https://plausible.io/js/plausible.js title: Plausible JS Script source hint: The URL of Plausbile Script (only needed when using a self hosted installation) order: 2 ================================================ FILE: server/modules/analytics/statcounter/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/statcounter/definition.yml ================================================ key: statcounter title: StatCounter description: See how Statcounter's easy-to-use features give you everything you need to understand your visitors and increase your website traffic. author: requarks.io logo: https://static.requarks.io/logo/statcountr.svg website: https://statcounter.com/ isAvailable: true props: projectId: type: String title: Project ID hint: Unique Project ID, found in the code snippet provided by StatCounter order: 1 securityToken: type: String title: Security Token hint: Security token, found in the code snippet provided by StatCounter order: 2 ================================================ FILE: server/modules/analytics/umami/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/umami/definition.yml ================================================ key: umami title: Umami Analytics v1 description: Umami is a simple, fast, privacy-focused alternative to Google Analytics. author: CDN18 logo: https://static.requarks.io/logo/umami.svg website: https://umami.is isAvailable: true props: websiteID: type: String title: Website ID order: 1 url: type: String title: Umami Server URL hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com) order: 2 ================================================ FILE: server/modules/analytics/umami2/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/umami2/definition.yml ================================================ key: umami2 title: Umami Analytics v2 description: Umami is a simple, fast, privacy-focused alternative to Google Analytics. author: CDN18 logo: https://static.requarks.io/logo/umami.svg website: https://umami.is isAvailable: true props: websiteID: type: String title: Website ID order: 1 url: type: String title: Umami Server URL hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com) order: 2 ================================================ FILE: server/modules/analytics/yandex/code.yml ================================================ head: | ================================================ FILE: server/modules/analytics/yandex/definition.yml ================================================ key: yandex title: Yandex Metrica description: From traffic trends to mouse movements – get a comprehensive understanding of your online audience and drive business growth. author: requarks.io logo: https://static.requarks.io/logo/yandex.svg website: https://metrica.yandex.com isAvailable: true props: tagNumber: type: String title: Tag Number hint: When creating the tag, select "CMS and website builders" and copy the provided Tag Number order: 1 ================================================ FILE: server/modules/authentication/auth0/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Auth0 Account // ------------------------------------ const Auth0Strategy = require('passport-auth0').Strategy module.exports = { init (passport, conf) { passport.use(conf.key, new Auth0Strategy({ domain: conf.domain, clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, extraParams, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile }) cb(null, user) } catch (err) { cb(err, null) } } )) }, logout (conf) { return `https://${conf.domain}/v2/logout?${new URLSearchParams({ client_id: conf.clientId, returnTo: WIKI.config.host }).toString()}` } } ================================================ FILE: server/modules/authentication/auth0/definition.yml ================================================ key: auth0 title: Auth0 description: Auth0 provides universal identity platform for web, mobile, IoT, and internal applications. author: requarks.io logo: https://static.requarks.io/logo/auth0.svg color: deep-orange website: https://auth0.com/ isAvailable: true useForm: false scopes: - openid - profile - email props: domain: type: String title: Domain hint: Your Auth0 domain (e.g. something.auth0.com) order: 1 clientId: type: String title: Client ID hint: Application Client ID order: 2 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 3 ================================================ FILE: server/modules/authentication/azure/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // Azure AD Account // ------------------------------------ const OIDCStrategy = require('passport-azure-ad').OIDCStrategy module.exports = { init (passport, conf) { // Workaround for Chrome's SameSite cookies // cookieSameSite needs useCookieInsteadOfSession to work correctly. // cookieEncryptionKeys is extracted from conf.cookieEncryptionKeyString. // It's a concatnation of 44-character length strings each of which represents a single pair of key/iv. // Valid cookieEncryptionKeys enables both cookieSameSite and useCookieInsteadOfSession. const keyArray = []; if (conf.cookieEncryptionKeyString) { let keyString = conf.cookieEncryptionKeyString; while (keyString.length >= 44) { keyArray.push({ key: keyString.substring(0, 32), iv: keyString.substring(32, 44) }); keyString = keyString.substring(44); } } passport.use(conf.key, new OIDCStrategy({ identityMetadata: conf.entryPoint, clientID: conf.clientId, redirectUrl: conf.callbackURL, responseType: 'id_token', responseMode: 'form_post', scope: ['profile', 'email', 'openid'], allowHttpForRedirectUrl: WIKI.IS_DEBUG, passReqToCallback: true, cookieSameSite: keyArray.length > 0, useCookieInsteadOfSession: keyArray.length > 0, cookieEncryptionKeys: keyArray }, async (req, iss, sub, profile, cb) => { const usrEmail = _.get(profile, '_json.email', null) || _.get(profile, '_json.preferred_username') try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { id: profile.oid, displayName: profile.displayName, email: usrEmail, picture: '' } }) if (conf.mapGroups) { const groups = _.get(profile, '_json.groups') if (groups && _.isArray(groups)) { const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } cb(null, user) } catch (err) { cb(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/azure/definition.yml ================================================ key: azure title: Azure Active Directory description: Azure Active Directory (Azure AD) is Microsoft’s multi-tenant, cloud-based directory, and identity management service that combines core directory services, application access management, and identity protection into a single solution. author: requarks.io logo: https://static.requarks.io/logo/azure.svg color: blue darken-3 website: https://azure.microsoft.com/services/active-directory/ isAvailable: true useForm: false scopes: - profile - email - openid props: entryPoint: type: String title: Identity Metadata Endpoint hint: The metadata endpoint provided by the Microsoft Identity Portal that provides the keys and other important information at runtime. order: 1 clientId: type: String title: Client ID hint: The client ID of your application in AAD (Azure Active Directory) order: 2 cookieEncryptionKeyString: type: String title: Cookie Encryption Key String hint: Random string with 44-character length. Setting this enables workaround for Chrome's SameSite cookies. order: 3 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false order: 4 ================================================ FILE: server/modules/authentication/cas/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // CAS Account // ------------------------------------ const CASStrategy = require('passport-cas').Strategy module.exports = { init (passport, conf) { passport.use(conf.key, new CASStrategy({ version: conf.casVersion, ssoBaseURL: conf.casUrl, serverBaseURL: conf.baseUrl, serviceURL: conf.callbackURL, passReqToCallback: true }, async (req, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, id: _.get(profile.attributes, conf.uniqueIdAttribute, profile.user), email: _.get(profile.attributes, conf.emailAttribute), name: _.get(profile.attributes, conf.displayNameAttribute, profile.user), picture: '' } }) cb(null, user) } catch (err) { cb(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/cas/definition.yml ================================================ key: cas title: CAS description: The Central Authentication Service (CAS) is a single sign-on protocol for the web. author: requarks.io logo: https://static.requarks.io/logo/cas.svg color: green darken-2 website: https://apereo.github.io/cas/ useForm: false isAvailable: true props: baseUrl: type: String title: Base URL hint: 'Base-URL of your WikiJS (for example: https://wiki.example.com)' order: 1 casUrl: type: String title: URL to the CAS Server hint: 'Base-URL of the CAS server, including context path. (for example: https://login.company.com/cas)' order: 2 casVersion: type: String title: CAS Version hint: 'The version of CAS to use' order: 3 enum: - CAS3.0 - CAS1.0 default: 'CAS3.0' emailAttribute: type: String title: Attribute key which contains the users email default: email order: 4 displayNameAttribute: type: String title: Attribute key which contains the users display name (leave empty if there is none) order: 5 uniqueIdAttribute: type: String title: Attribute key which contains the unique identifier of a user. (if empty, username will be used) order: 6 ================================================ FILE: server/modules/authentication/discord/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Discord Account // ------------------------------------ const DiscordStrategy = require('passport-discord').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new DiscordStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none', callbackURL: conf.callbackURL, scope: 'identify email guilds', passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { if (conf.guildId && !_.some(profile.guilds, { id: conf.guildId })) { throw new WIKI.Error.AuthLoginFailed() } const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, displayName: profile.username, picture: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/discord/definition.yml ================================================ key: discord title: Discord description: Discord is a proprietary freeware VoIP application designed for gaming communities, that specializes in text, video and audio communication between users in a chat channel. author: requarks.io logo: https://static.requarks.io/logo/discord.svg color: indigo lighten-2 website: https://discord.com/ isAvailable: true useForm: false props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 guildId: type: String title: Server ID hint: Optional - Your unique server identifier, such that only members are authorized order: 3 ================================================ FILE: server/modules/authentication/dropbox/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Dropbox Account // ------------------------------------ const DropboxStrategy = require('passport-dropbox-oauth2').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new DropboxStrategy({ apiVersion: '2', clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, '_json.profile_photo_url', '') } }) cb(null, user) } catch (err) { cb(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/dropbox/definition.yml ================================================ key: dropbox title: Dropbox description: Dropbox is a file hosting service that offers cloud storage, file synchronization, personal cloud, and client software. author: requarks.io logo: https://static.requarks.io/logo/dropbox.svg color: blue darken-2 website: https://dropbox.com isAvailable: true useForm: false props: clientId: type: String title: App Key hint: Application Client ID order: 1 clientSecret: type: String title: App Secret hint: Application Client Secret order: 2 ================================================ FILE: server/modules/authentication/facebook/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Facebook Account // ------------------------------------ const FacebookStrategy = require('passport-facebook').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new FacebookStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, profileFields: ['id', 'displayName', 'email', 'photos'], authType: 'reauthenticate', passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'photos[0].value', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/facebook/definition.yml ================================================ key: facebook title: Facebook description: Facebook is an online social media and social networking service company. author: requarks.io logo: https://static.requarks.io/logo/facebook.svg color: indigo website: https://facebook.com/ isAvailable: true useForm: false scopes: - email props: clientId: type: String title: App ID hint: Application ID order: 1 clientSecret: type: String title: App Secret hint: Application Secret order: 2 ================================================ FILE: server/modules/authentication/firebase/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Firebase Account // ------------------------------------ // INCOMPLETE / TODO const FirebaseStrategy = require('passport-github2').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new FirebaseStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, scope: ['user:email'] }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'photos[0].value', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/firebase/definition.yml ================================================ key: firebase title: Firebase description: Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business. author: requarks.io logo: https://static.requarks.io/logo/firebase.svg color: yellow darken-3 website: https://firebase.google.com/ isAvailable: false useForm: false props: {} ================================================ FILE: server/modules/authentication/github/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // GitHub Account // ------------------------------------ const GitHubStrategy = require('passport-github2').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { let githubConfig = { clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, scope: ['user:email'], passReqToCallback: true } if (conf.useEnterprise) { githubConfig.authorizationURL = `https://${conf.enterpriseDomain}/login/oauth/authorize` githubConfig.tokenURL = `https://${conf.enterpriseDomain}/login/oauth/access_token` githubConfig.userProfileURL = conf.enterpriseUserEndpoint githubConfig.userEmailURL = `${conf.enterpriseUserEndpoint}/emails` } passport.use(conf.key, new GitHubStrategy(githubConfig, async (req, accessToken, refreshToken, profile, cb) => { try { WIKI.logger.info(`GitHub OAuth: Processing profile for user ${profile.id || profile.username}`) // Ensure email is available - passport-github2 should fetch it automatically with user:email scope // but we'll log a warning if it's missing if (!profile.emails || (Array.isArray(profile.emails) && profile.emails.length === 0)) { WIKI.logger.warn(`GitHub OAuth: No email found in profile for user ${profile.id || profile.username}. Make sure 'user:email' scope is granted.`) } const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'photos[0].value', '') } }) WIKI.logger.info(`GitHub OAuth: Successfully authenticated user ${user.email}`) cb(null, user) } catch (err) { WIKI.logger.warn(`GitHub OAuth: Authentication failed for strategy ${req.params.strategy}:`, err) // Provide more user-friendly error messages if (err.message && err.message.includes('email')) { cb(new Error('GitHub authentication failed: Email address is required but not available. Please ensure your GitHub account has a verified email address and grant email access permissions.'), null) } else if (err instanceof WIKI.Error.AuthAccountBanned) { cb(err, null) } else { cb(new Error(`GitHub authentication failed: ${err.message || 'Unknown error'}`), null) } } } )) } } ================================================ FILE: server/modules/authentication/github/definition.yml ================================================ key: github title: GitHub description: GitHub Inc. is a web-based hosting service for version control using Git. author: requarks.io logo: https://static.requarks.io/logo/github.svg color: grey darken-3 website: https://github.com isAvailable: true useForm: false props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 useEnterprise: type: Boolean title: Use GitHub Enterprise hint: Enable if you're using the self-hosted GitHub Enterprise version default: false order: 3 enterpriseDomain: type: String title: GitHub Enterprise Domain hint: GitHub Enterprise Only - Domain of your installation (e.g. github.company.com). Leave blank otherwise. default: '' order: 4 enterpriseUserEndpoint: type: String title: GitHub Enterprise User Endpoint hint: GitHub Enterprise Only - Endpoint to fetch user details (e.g. https://api.github.com/user). Leave blank otherwise. default: 'https://api.github.com/user' order: 5 ================================================ FILE: server/modules/authentication/gitlab/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // GitLab Account // ------------------------------------ const GitLabStrategy = require('passport-gitlab2').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new GitLabStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, baseURL: conf.baseUrl, authorizationURL: conf.authorizationURL || (conf.baseUrl + '/oauth/authorize'), tokenURL: conf.tokenURL || (conf.baseUrl + '/oauth/token'), scope: ['read_user'], passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'avatarUrl', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/gitlab/definition.yml ================================================ key: gitlab title: GitLab description: GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager providing wiki, issue-tracking and CI/CD pipeline features. author: requarks.io logo: https://static.requarks.io/logo/gitlab.svg color: deep-orange website: https://gitlab.com isAvailable: true useForm: false props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 baseUrl: type: String title: Base URL hint: For self-managed GitLab instances, define the base URL (e.g. https://gitlab.example.com). Leave default for GitLab.com SaaS (https://gitlab.com). default: https://gitlab.com order: 3 authorizationURL: type: String title: Authorization URL hint: For self-managed GitLab instances, define an alternate authorization URL (e.g. http://example.com/oauth/authorize). Leave empty otherwise. order: 4 tokenURL: type: String title: Token URL hint: For self-managed GitLab instances, define an alternate token URL (e.g. http://example.com/oauth/token). Leave empty otherwise. order: 5 ================================================ FILE: server/modules/authentication/google/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Google ID Account // ------------------------------------ const GoogleStrategy = require('passport-google-oauth20').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { const strategy = new GoogleStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { WIKI.logger.info(`Google OAuth: Processing profile for user ${profile.id || profile.displayName}`) // Validate hosted domain if configured if (conf.hostedDomain && profile._json.hd !== conf.hostedDomain) { throw new Error(`Google authentication failed: User must be from domain ${conf.hostedDomain}, but got ${profile._json.hd || 'unknown'}`) } const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'photos[0].value', '') } }) WIKI.logger.info(`Google OAuth: Successfully authenticated user ${user.email}`) cb(null, user) } catch (err) { WIKI.logger.warn(`Google OAuth: Authentication failed for strategy ${req.params.strategy}:`, err) // Provide more user-friendly error messages if (err.message && err.message.includes('domain')) { cb(new Error(`Google authentication failed: ${err.message}`), null) } else if (err.message && err.message.includes('email')) { cb(new Error('Google authentication failed: Email address is required but not available. Please ensure your Google account has a verified email address.'), null) } else if (err instanceof WIKI.Error.AuthAccountBanned) { cb(err, null) } else { cb(new Error(`Google authentication failed: ${err.message || 'Unknown error'}`), null) } } }) if (conf.hostedDomain) { strategy.authorizationParams = function(options) { return { hd: conf.hostedDomain } } } passport.use(conf.key, strategy) }, logout (conf) { return '/' } } ================================================ FILE: server/modules/authentication/google/definition.yml ================================================ key: google title: Google description: Google specializes in Internet-related services and products, which include online advertising technologies, search engine, cloud computing, software, and hardware. author: requarks.io logo: https://static.requarks.io/logo/google.svg color: red darken-1 website: https://console.developers.google.com/ isAvailable: true useForm: false scopes: - profile - email - openid props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 hostedDomain: type: String title: Hosted Domain hint: (optional) Only for G Suite hosted domain. Leave empty otherwise. order: 3 ================================================ FILE: server/modules/authentication/keycloak/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // Keycloak Account // ------------------------------------ const KeycloakStrategy = require('@exlinc/keycloak-passport') module.exports = { init (passport, conf) { passport.use(conf.key, new KeycloakStrategy({ authorizationURL: conf.authorizationURL, userInfoURL: conf.userInfoURL, tokenURL: conf.tokenURL, host: conf.host, realm: conf.realm, clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, results, profile, cb) => { let displayName = profile.username if (_.isString(profile.fullName) && profile.fullName.length > 0) { displayName = profile.fullName } try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { id: profile.keycloakId, email: profile.email, name: displayName, picture: '' } }) req.session.keycloak_id_token = results.id_token cb(null, user) } catch (err) { cb(err, null) } }) ) }, logout (conf, context) { if (!conf.logoutUpstream) { return '/' } else if (conf.logoutURL && conf.logoutURL.length > 5) { const idToken = context.req.session.keycloak_id_token const redirURL = encodeURIComponent(WIKI.config.host) if (conf.logoutUpstreamRedirectLegacy) { // keycloak < 18 return `${conf.logoutURL}?redirect_uri=${redirURL}` } else if (idToken) { // keycloak 18+ return `${conf.logoutURL}?post_logout_redirect_uri=${redirURL}&id_token_hint=${idToken}` } else { // fall back to no redirect if keycloak_id_token isn't available return conf.logoutURL } } else { WIKI.logger.warn('Keycloak logout URL is not configured!') return '/' } } } ================================================ FILE: server/modules/authentication/keycloak/definition.yml ================================================ key: keycloak title: Keycloak description: Keycloak is an open source software product to allow single sign-on with Identity Management and Access Management aimed at modern applications and services. author: D4uS1 logo: https://static.requarks.io/logo/keycloak.svg color: blue-grey darken-2 website: https://www.keycloak.org/ useForm: false isAvailable: true scopes: - openid - profile - email props: host: type: String title: Host hint: e.g. https://your.keycloak-host.com order: 1 realm: type: String title: Realm hint: The realm this application belongs to. order: 2 clientId: type: String title: Client ID hint: Application Client ID order: 3 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 4 authorizationURL: type: String title: Authorization Endpoint URL hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/auth order: 5 tokenURL: type: String title: Token Endpoint URL hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/token order: 6 userInfoURL: type: String title: User Info Endpoint URL hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/userinfo order: 7 logoutUpstream: type: Boolean title: Logout from Keycloak on Logout hint: Should the user be redirected to Keycloak logout mechanism upon logout order: 8 logoutURL: type: String title: Logout Endpoint URL hint: e.g. https://KEYCLOAK-HOST/realms/YOUR-REALM/protocol/openid-connect/logout order: 9 logoutUpstreamRedirectLegacy: type: Boolean title: Legacy Logout Redirect hint: Pass the legacy 'redirect_uri' parameter to the logout endpoint. Leave disabled for Keycloak 18 and above. order: 10 ================================================ FILE: server/modules/authentication/ldap/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // LDAP Account // ------------------------------------ const LdapStrategy = require('passport-ldapauth').Strategy const fs = require('fs') const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new LdapStrategy({ server: { url: conf.url, bindDn: conf.bindDn, bindCredentials: conf.bindCredentials, searchBase: conf.searchBase, searchFilter: conf.searchFilter, tlsOptions: getTlsOptions(conf), ...conf.mapGroups && { groupSearchBase: conf.groupSearchBase, groupSearchFilter: conf.groupSearchFilter, groupSearchScope: conf.groupSearchScope, groupDnProperty: conf.groupDnProperty, groupSearchAttributes: [conf.groupNameField] }, includeRaw: true }, usernameField: 'email', passwordField: 'password', passReqToCallback: true }, async (req, profile, cb) => { try { const userId = _.get(profile, conf.mappingUID, null) if (!userId) { throw new Error('Invalid Unique ID field mapping!') } const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { id: userId, email: String(_.get(profile, conf.mappingEmail, '')).split(',')[0], displayName: _.get(profile, conf.mappingDisplayName, '???'), picture: _.get(profile, `_raw.${conf.mappingPicture}`, '') } }) // map users LDAP groups to wiki groups with the same name, and remove any groups that don't match LDAP if (conf.mapGroups) { const ldapGroups = _.get(profile, '_groups') if (ldapGroups && _.isArray(ldapGroups)) { const groups = ldapGroups.map(g => g[conf.groupNameField]) const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } cb(null, user) } catch (err) { if (WIKI.config.flags.ldapdebug) { WIKI.logger.warn('LDAP LOGIN ERROR (c2): ', err) } cb(err, null) } } )) } } function getTlsOptions(conf) { if (!conf.tlsEnabled) { return {} } if (!conf.tlsCertPath) { return { rejectUnauthorized: conf.verifyTLSCertificate } } const caList = [] if (conf.verifyTLSCertificate) { caList.push(fs.readFileSync(conf.tlsCertPath)) } return { rejectUnauthorized: conf.verifyTLSCertificate, ca: caList } } ================================================ FILE: server/modules/authentication/ldap/definition.yml ================================================ key: ldap title: LDAP / Active Directory description: Active Directory is a directory service that Microsoft developed for the Windows domain networks. author: requarks.io logo: https://static.requarks.io/logo/active-directory.svg color: blue darken-3 website: https://www.microsoft.com/windowsserver isAvailable: true useForm: true usernameType: username props: url: title: LDAP URL type: String default: 'ldap://serverhost:389' hint: (e.g. ldap://serverhost:389 or ldaps://serverhost:636) order: 1 bindDn: title: Admin Bind DN type: String default: cn='root' hint: The distinguished name (dn) of the account used for binding. maxWidth: 600 order: 2 bindCredentials: title: Admin Bind Credentials type: String hint: The password of the account used above for binding. maxWidth: 600 order: 3 searchBase: title: Search Base type: String default: 'o=users,o=example.com' hint: The base DN from which to search for users. order: 4 searchFilter: title: Search Filter type: String default: '(uid={{username}})' hint: The query to use to match username. {{username}} must be present and will be interpolated with the user provided username when performing the LDAP search. order: 5 tlsEnabled: title: Use TLS type: Boolean default: false order: 6 verifyTLSCertificate: title: Verify TLS Certificate type: Boolean default: true order: 7 tlsCertPath: title: TLS Certificate Path type: String hint: Absolute path to the TLS certificate on the server. order: 8 mappingUID: title: Unique ID Field Mapping type: String default: 'uid' hint: The field storing the user unique identifier. Usually "uid" or "sAMAccountName". maxWidth: 500 order: 20 mappingEmail: title: Email Field Mapping type: String default: 'mail' hint: The field storing the user email. Usually "mail". maxWidth: 500 order: 21 mappingDisplayName: title: Display Name Field Mapping type: String default: 'displayName' hint: The field storing the user display name. Usually "displayName" or "cn". maxWidth: 500 order: 22 mappingPicture: title: Avatar Picture Field Mapping type: String default: 'jpegPhoto' hint: The field storing the user avatar picture. Usually "jpegPhoto" or "thumbnailPhoto". maxWidth: 500 order: 23 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the users LDAP/Active Directory groups. Group Search Base must also be defined for this to work. Note this will remove any groups the user has that doesn't match an LDAP/Active Directory group. default: false order: 24 groupSearchBase: type: String title: Group Search Base hint: The base DN from which to search for groups. default: OU=groups,dc=example,dc=com order: 25 groupSearchFilter: type: String title: Group Search Filter hint: LDAP search filter for groups. (member={{dn}}) will use the distinguished name of the user and will work in most cases. default: (member={{dn}}) order: 26 groupSearchScope: type: String title: Group Search Scope hint: How far from the Group Search Base to search for groups. sub (default) will search the entire subtree. base, will only search the Group Search Base dn. one, will search the Group Search Base dn and one additional level. default: sub order: 27 groupDnProperty: type: String title: Group DN Property hint: The property of user object to use in {{dn}} interpolation of Group Search Filter. default: dn order: 28 groupNameField: type: String title: Group Name Field hint: The field that contains the name of the LDAP group to match on, usually "name" or "cn". default: name order: 29 ================================================ FILE: server/modules/authentication/local/authentication.js ================================================ const bcrypt = require('bcryptjs-then') /* global WIKI */ // ------------------------------------ // Local Account // ------------------------------------ const LocalStrategy = require('passport-local').Strategy module.exports = { init (passport, conf) { passport.use('local', new LocalStrategy({ usernameField: 'email', passwordField: 'password' }, async (uEmail, uPassword, done) => { try { const user = await WIKI.models.users.query().findOne({ email: uEmail.toLowerCase(), providerKey: 'local' }) if (user) { await user.verifyPassword(uPassword) if (!user.isActive) { done(new WIKI.Error.AuthAccountBanned(), null) } else if (!user.isVerified) { done(new WIKI.Error.AuthAccountNotVerified(), null) } else { done(null, user) } } else { // Fake verify password to mask timing differences await bcrypt.compare((Math.random() + 1).toString(36), '$2a$12$irXbAcQSY59pcQQfNQpY8uyhfSw48nzDikAmr60drI501nR.PuBx2') done(new WIKI.Error.AuthLoginFailed(), null) } } catch (err) { done(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/local/definition.yml ================================================ key: local title: Local Database description: Built-in authentication for Wiki.js author: requarks.io logo: https://static.requarks.io/logo/wikijs.svg color: primary website: https://wiki.js.org isAvailable: true useForm: true usernameType: email props: {} ================================================ FILE: server/modules/authentication/microsoft/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Microsoft Account // ------------------------------------ const WindowsLiveStrategy = require('passport-microsoft').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new WindowsLiveStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, scope: ['User.Read', 'email', 'openid', 'profile'], passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'photos[0].value', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/microsoft/definition.yml ================================================ key: microsoft title: Microsoft description: Microsoft is a software company, best known for it's Windows, Office, Azure, Xbox and Surface products. author: requarks.io logo: https://static.requarks.io/logo/microsoft.svg color: blue website: https://apps.dev.microsoft.com/ isAvailable: false useForm: false scopes: - openid - profile - email props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 ================================================ FILE: server/modules/authentication/oauth2/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // OAuth2 Account // ------------------------------------ const OAuth2Strategy = require('passport-oauth2').Strategy module.exports = { init (passport, conf) { var client = new OAuth2Strategy({ authorizationURL: conf.authorizationURL, tokenURL: conf.tokenURL, clientID: conf.clientId, clientSecret: conf.clientSecret, userInfoURL: conf.userInfoURL, callbackURL: conf.callbackURL, passReqToCallback: true, scope: conf.scope, state: conf.enableCSRFProtection }, async (req, accessToken, refreshToken, profile, cb) => { try { const picture = _.get(profile, conf.pictureClaim, '') const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, id: _.get(profile, conf.userIdClaim), displayName: _.get(profile, conf.displayNameClaim, '???'), email: _.get(profile, conf.emailClaim), picture: picture } }) if (conf.mapGroups) { const groups = _.get(profile, conf.groupsClaim) if (groups && _.isArray(groups)) { const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } cb(null, user) } catch (err) { cb(err, null) } }) client.userProfile = function (accesstoken, done) { this._oauth2._useAuthorizationHeaderForGET = !conf.useQueryStringForAccessToken this._oauth2.get(conf.userInfoURL, accesstoken, (err, data) => { if (err) { return done(err) } try { data = JSON.parse(data) } catch (e) { return done(e) } done(null, data) }) } passport.use(conf.key, client) }, logout (conf) { if (!conf.logoutURL) { return '/' } else { return conf.logoutURL } } } ================================================ FILE: server/modules/authentication/oauth2/definition.yml ================================================ key: oauth2 title: Generic OAuth2 description: OAuth 2.0 is the industry-standard protocol for authorization. author: requarks.io logo: https://static.requarks.io/logo/oauth2.svg color: blue-grey darken-2 website: https://oauth.net/2/ isAvailable: true useForm: false props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 authorizationURL: type: String title: Authorization Endpoint URL hint: Application Authorization Endpoint URL order: 3 tokenURL: type: String title: Token Endpoint URL hint: Application Token Endpoint URL order: 4 userInfoURL: type: String title: User Info Endpoint URL hint: User Info Endpoint URL order: 5 userIdClaim: type: String title: ID Claim hint: Field containing the user ID default: id maxWidth: 500 order: 6 displayNameClaim: type: String title: Display Name Claim hint: Field containing user display name default: displayName maxWidth: 500 order: 7 emailClaim: type: String title: Email Claim hint: Field containing the user email address default: email maxWidth: 500 order: 8 pictureClaim: type: String title: Picture Claim hint: Field containing the user avatar URL default: picture maxWidth: 500 order: 9 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false order: 10 groupsClaim: type: String title: Groups Claim hint: Field containing the group names default: groups maxWidth: 500 order: 11 logoutURL: type: String title: Logout URL hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. order: 12 scope: type: String title: Scope hint: (optional) Application Client permission scopes. order: 13 useQueryStringForAccessToken: type: Boolean default: false title: Pass access token via GET query string to User Info Endpoint hint: (optional) Pass the access token in an `access_token` parameter attached to the GET query string of the User Info Endpoint URL. Otherwise the access token will be passed in the Authorization header. order: 14 enableCSRFProtection: type: Boolean default: true title: Enable CSRF protection hint: Pass a nonce state parameter during authentication to protect against CSRF attacks. order: 15 ================================================ FILE: server/modules/authentication/oidc/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // OpenID Connect Account // ------------------------------------ const OpenIDConnectStrategy = require('passport-openidconnect').Strategy module.exports = { init (passport, conf) { passport.use(conf.key, new OpenIDConnectStrategy({ authorizationURL: conf.authorizationURL, tokenURL: conf.tokenURL, clientID: conf.clientId, clientSecret: conf.clientSecret, issuer: conf.issuer, userInfoURL: conf.userInfoURL, callbackURL: conf.callbackURL, passReqToCallback: true, skipUserProfile: conf.skipUserProfile, acrValues: conf.acrValues }, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => { const profile = Object.assign({}, idProfile, uiProfile) const picture = _.get(profile, '_json.' + conf.pictureClaim, '') try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, email: _.get(profile, '_json.' + conf.emailClaim), displayName: _.get(profile, '_json.' + conf.displayNameClaim, ''), picture: picture } }) if (conf.mapGroups) { const groups = _.get(profile, '_json.' + conf.groupsClaim) if (groups && _.isArray(groups)) { const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } cb(null, user) } catch (err) { cb(err, null) } }) ) }, logout (conf) { if (!conf.logoutURL) { return '/' } else { return conf.logoutURL } } } ================================================ FILE: server/modules/authentication/oidc/definition.yml ================================================ key: oidc title: Generic OpenID Connect / OAuth2 description: OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. author: requarks.io logo: https://static.requarks.io/logo/oidc.svg color: blue-grey darken-2 website: http://openid.net/connect/ isAvailable: true useForm: false scopes: - openid - profile - email props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 authorizationURL: type: String title: Authorization Endpoint URL hint: Application Authorization Endpoint URL order: 3 tokenURL: type: String title: Token Endpoint URL hint: Application Token Endpoint URL order: 4 userInfoURL: type: String title: User Info Endpoint URL hint: User Info Endpoint URL order: 5 skipUserProfile: type: Boolean default: false title: Skip User Profile hint: Skips call to the OIDC UserInfo endpoint order: 6 issuer: type: String title: Issuer hint: Issuer URL order: 7 emailClaim: type: String title: Email Claim hint: Field containing the email address default: email maxWidth: 500 order: 8 displayNameClaim: type: String title: Display Name Claim hint: Field containing the user display name default: displayName maxWidth: 500 order: 9 pictureClaim: type: String title: Picture Claim hint: Field containing the user avatar URL default: picture maxWidth: 500 order: 10 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false order: 11 groupsClaim: type: String title: Groups Claim hint: Field containing the group names default: groups maxWidth: 500 order: 12 logoutURL: type: String title: Logout URL hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. order: 13 acrValues: type: String title: ACR Values hint: (optional) Authentication Context Class Reference order: 14 ================================================ FILE: server/modules/authentication/okta/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Okta Account // ------------------------------------ const OktaStrategy = require('passport-okta-oauth').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new OktaStrategy({ audience: conf.audience, clientID: conf.clientId, clientSecret: conf.clientSecret, idp: conf.idp, callbackURL: conf.callbackURL, response_type: 'code', passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, '_json.profile', '') } }) cb(null, user) } catch (err) { cb(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/okta/definition.yml ================================================ key: okta title: Okta description: Okta provide secure identity management and single sign-on to any application. author: requarks.io logo: https://static.requarks.io/logo/okta.svg color: blue darken-1 website: https://www.okta.com/ isAvailable: true useForm: false scopes: - profile - email - openid props: audience: title: Org URL type: String hint: Okta organization URL (e.g. https://example.okta.com, https://example.oktapreview.com), found on the Developer Dashboard, in the upper right. order: 1 clientId: title: Client ID type: String hint: 20 chars alphanumeric string maxWidth: 400 order: 2 clientSecret: title: Client Secret type: String hint: 40 chars alphanumeric string with a hyphen(s) maxWidth: 600 order: 3 idp: title: Identity Provider ID (idp) type: String hint: (Optional) - 20 chars alphanumeric string maxWidth: 400 order: 4 ================================================ FILE: server/modules/authentication/rocketchat/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // Rocket.chat Account // ------------------------------------ const OAuth2Strategy = require('passport-oauth2').Strategy module.exports = { init (passport, conf) { const siteURL = conf.siteURL.slice(-1) === '/' ? conf.siteURL.slice(0, -1) : conf.siteURL const strategyInstance = new OAuth2Strategy({ authorizationURL: `${siteURL}/oauth/authorize`, tokenURL: `${siteURL}/oauth/token`, clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile }) cb(null, user) } catch (err) { cb(err, null) } }) strategyInstance.userProfile = function (accessToken, cb) { this._oauth2.get(`${siteURL}/api/v1/me`, accessToken, (err, body, res) => { if (err) { WIKI.logger.warn('Rocket.chat - Failed to fetch user profile.') return cb(err) } try { const usr = JSON.parse(body) cb(null, { id: usr._id, displayName: _.isEmpty(usr.name) ? usr.username : usr.name, email: usr.emails[0].address, picture: usr.avatarUrl }) } catch (err) { WIKI.logger.warn('Rocket.chat - Failed to parse user profile.') cb(err) } }) } passport.use(conf.key, strategyInstance) }, logout (conf) { if (!conf.logoutURL) { return '/' } else { return conf.logoutURL } } } ================================================ FILE: server/modules/authentication/rocketchat/definition.yml ================================================ key: rocketchat title: Rocket.chat description: Communicate and collaborate with your team, share files, chat in real-time, or switch to video/audio conferencing. author: requarks.io logo: https://static.requarks.io/logo/rocketchat.svg color: red accent-3 website: https://rocket.chat/ isAvailable: true useForm: false scopes: - openid - profile - email props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 siteURL: type: String title: Rocket.chat Site URL hint: The base URL of your Rocket.chat site (e.g. https://example.rocket.chat) order: 3 ================================================ FILE: server/modules/authentication/saml/authentication.js ================================================ const _ = require('lodash') /* global WIKI */ // ------------------------------------ // SAML Account // ------------------------------------ const SAMLStrategy = require('passport-saml').Strategy module.exports = { init (passport, conf) { const samlConfig = { callbackUrl: conf.callbackURL, entryPoint: conf.entryPoint, issuer: conf.issuer, cert: (conf.cert || '').split('|'), signatureAlgorithm: conf.signatureAlgorithm, digestAlgorithm: conf.digestAlgorithm, identifierFormat: conf.identifierFormat, wantAssertionsSigned: conf.wantAssertionsSigned, acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs), disableRequestedAuthnContext: conf.disableRequestedAuthnContext, authnContext: (conf.authnContext || '').split('|'), racComparison: conf.racComparison, forceAuthn: conf.forceAuthn, passive: conf.passive, providerName: conf.providerName, skipRequestCompression: conf.skipRequestCompression, authnRequestBinding: conf.authnRequestBinding, passReqToCallback: true } if (!_.isEmpty(conf.audience)) { samlConfig.audience = conf.audience } if (!_.isEmpty(conf.privateKey)) { samlConfig.privateKey = conf.privateKey } if (!_.isEmpty(conf.decryptionPvk)) { samlConfig.decryptionPvk = conf.decryptionPvk } passport.use(conf.key, new SAMLStrategy(samlConfig, async (req, profile, cb) => { try { const userId = _.get(profile, [conf.mappingUID], null) || _.get(profile, 'nameID', null) if (!userId) { throw new Error('Invalid or Missing Unique ID field!') } const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { id: userId, email: _.get(profile, conf.mappingEmail, ''), displayName: _.get(profile, conf.mappingDisplayName, '???'), picture: _.get(profile, conf.mappingPicture, '') } }) // map users provider groups to wiki groups with the same name, and remove any groups that don't match // Code copied from the LDAP implementation with a slight variation on the field we extract the value from // In SAML v2 groups come in profile.attributes and can be 1 string or an array of strings if (conf.mapGroups) { const maybeArrayOfGroups = _.get(profile.attributes, conf.mappingGroups) const groups = (maybeArrayOfGroups && !_.isArray(maybeArrayOfGroups)) ? [maybeArrayOfGroups] : maybeArrayOfGroups if (groups && _.isArray(groups)) { const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } cb(null, user) } catch (err) { cb(err, null) } }) ) } } ================================================ FILE: server/modules/authentication/saml/definition.yml ================================================ key: saml title: SAML 2.0 description: Security Assertion Markup Language 2.0 (SAML 2.0) is a version of the SAML standard for exchanging authentication and authorization data between security domains. author: requarks.io logo: https://static.requarks.io/logo/saml.svg color: red darken-3 website: https://wiki.oasis-open.org/security/FrontPage isAvailable: true useForm: false props: entryPoint: type: String title: Entry Point hint: Identity provider entrypoint (URL) order: 1 issuer: type: String title: Issuer hint: Issuer string to supply to Identity Provider order: 2 audience: type: String title: Audience hint: Expected SAML response Audience (if not provided, audience won't be verified) order: 3 cert: type: String title: Certificate hint: Public PEM-encoded X.509 signing certificate. If the provider has multiple certificates that are valid, join them together using the | pipe symbol. multiline: true order: 4 privateKey: type: String title: Private Key hint: PEM formatted key used to sign the certificate. multiline: true order: 5 decryptionPvk: type: String title: Decryption Private Key hint: (Optional) - Private key that will be used to attempt to decrypt any encrypted assertions that are received. multiline: true order: 6 signatureAlgorithm: type: String title: Signature Algorithm hint: Signature algorithm used for signing requests maxWidth: 400 order: 7 default: sha1 enum: - sha1 - sha256 - sha512 digestAlgorithm: type: String title: Digest Algorithm hint: Digest algorithm used to provide a digest for the signed data object maxWidth: 400 order: 8 default: sha1 enum: - sha1 - sha256 - sha512 identifierFormat: type: String title: Name Identifier format default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' order: 20 wantAssertionsSigned: type: Boolean title: Always sign assertions hint: If enabled, add WantAssertionsSigned="true" to the metadata, to specify that the IdP should always sign the assertions. default: false order: 21 acceptedClockSkewMs: type: Number title: Accepted Clock Skew Milleseconds hint: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely. default: 0 order: 22 disableRequestedAuthnContext: type: Boolean title: Disable Requested Auth Context hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers. default: false order: 23 authnContext: type: String title: Auth Context hint: Name identifier format to request auth context. For multiple values, join them together using the | pipe symbol. default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport order: 24 racComparison: type: String title: RAC Comparison Type hint: Requested Authentication Context comparison type. maxWidth: 400 order: 25 default: exact enum: - exact - minimum - maximum - better forceAuthn: type: Boolean title: Force Initial Re-authentication hint: If enabled, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, even if they possess a valid session. default: false order: 26 passive: type: Boolean title: Passive hint: If enabled, the initial SAML request from the service provider specifies that the IdP should prevent visible user interaction. default: false order: 27 providerName: type: String title: Provider Name hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider. default: wiki.js order: 28 skipRequestCompression: type: Boolean title: Skip Request Compression hint: If enabled, the SAML request from the service provider won't be compressed. default: false order: 29 authnRequestBinding: type: String title: Request Binding hint: Binding used for request authentication from IDP. maxWidth: 400 order: 30 default: 'HTTP-POST' enum: - HTTP-Redirect - HTTP-POST mappingUID: title: Unique ID Field Mapping type: String default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' hint: The field storing the user unique identifier. Can be a variable name or a URI-formatted string. order: 40 mappingEmail: title: Email Field Mapping type: String default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' hint: The field storing the user email. Can be a variable name or a URI-formatted string. order: 41 mappingDisplayName: title: Display Name Field Mapping type: String default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' hint: The field storing the user display name. Can be a variable name or a URI-formatted string. order: 42 mappingPicture: title: Avatar Picture Field Mapping type: String default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/picture' hint: The field storing the user avatar picture. Can be a variable name or a URI-formatted string. order: 43 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the provider user groups. User Groups Field Mapping must also be defined for this to work. Note this will remove any groups the user has that doesn't match any group from the provider. default: false order: 44 mappingGroups: title: User Groups Field Mapping type: String default: 'memberOf' hint: The field storing the user groups attribute (when Map Groups is enabled). Can be a variable name or a URI-formatted string. order: 45 ================================================ FILE: server/modules/authentication/slack/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Slack Account // ------------------------------------ const SlackStrategy = require('passport-slack-oauth2').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new SlackStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, team: conf.team, scope: ['identity.basic', 'identity.email', 'identity.avatar'], passReqToCallback: true }, async (req, accessToken, refreshToken, { user: userProfile }, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...userProfile, picture: _.get(userProfile, 'image_48', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/slack/definition.yml ================================================ key: slack title: Slack description: Slack is a cloud-based set of proprietary team collaboration tools and services. author: requarks.io logo: https://static.requarks.io/logo/slack.svg color: green website: https://api.slack.com/docs/oauth isAvailable: true useForm: false scopes: - identity.basic - identity.email - identity.avatar props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 team: type: String title: Team / Workspace ID hint: Optional - Your unique team (workspace) identifier order: 3 ================================================ FILE: server/modules/authentication/twitch/authentication.js ================================================ /* global WIKI */ // ------------------------------------ // Twitch Account // ------------------------------------ const TwitchStrategy = require('passport-twitch-strategy').Strategy const _ = require('lodash') module.exports = { init (passport, conf) { passport.use(conf.key, new TwitchStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, passReqToCallback: true }, async (req, accessToken, refreshToken, profile, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, profile: { ...profile, picture: _.get(profile, 'profile_image_url', '') } }) cb(null, user) } catch (err) { cb(err, null) } } )) } } ================================================ FILE: server/modules/authentication/twitch/definition.yml ================================================ key: twitch title: Twitch description: Twitch is a live streaming video platform. author: requarks.io logo: https://static.requarks.io/logo/twitch.svg color: indigo darken-2 website: https://dev.twitch.tv/docs/authentication/ isAvailable: true useForm: false scopes: - 'user:read:email' props: clientId: type: String title: Client ID hint: Application Client ID order: 1 clientSecret: type: String title: Client Secret hint: Application Client Secret order: 2 ================================================ FILE: server/modules/comments/artalk/code.yml ================================================ main: |
head: | body: | ================================================ FILE: server/modules/comments/artalk/definition.yml ================================================ key: artalk title: Artalk description: A light-weight self-hosted comment system. author: CDN18 logo: https://static.requarks.io/logo/artalk.png website: https://artalk.js.org codeTemplate: true isAvailable: true props: server: type: String title: Artalk Backend URL default: '' hint: 'Publicly accessible URL of your Artalk instance. It should start with http/https and omit the trailing slash. (e.g. https://artalk.example.com)' maxWidth: 650 order: 1 siteName: type: String title: Site Name default: '' hint: 'The name of this site configured in the artalk backend. Leave empty to use default site.' maxWidth: 450 order: 2 ================================================ FILE: server/modules/comments/commento/code.yml ================================================ main: |
body: | ================================================ FILE: server/modules/comments/commento/definition.yml ================================================ key: commento title: Commento description: A fast, privacy-focused commenting platform. author: requarks.io logo: https://static.requarks.io/logo/commento.svg website: https://commento.io/ codeTemplate: true isAvailable: true props: instanceUrl: type: String title: Instance URL default: 'https://cdn.commento.io' hint: The URL (without a trailing slash) to the Commento instance. Leave the default https://cdn.commento.io if using the cloud-hosted version. order: 1 ================================================ FILE: server/modules/comments/default/comment.js ================================================ const md = require('markdown-it') const { full: mdEmoji } = require('markdown-it-emoji') const { JSDOM } = require('jsdom') const createDOMPurify = require('dompurify') const _ = require('lodash') const { AkismetClient } = require('akismet-api') const moment = require('moment') /* global WIKI */ const window = new JSDOM('').window const DOMPurify = createDOMPurify(window) let akismetClient = null const mkdown = md({ html: false, breaks: true, linkify: true, highlight(str, lang) { return `
${_.escape(str)}
` } }) mkdown.use(mdEmoji) // ------------------------------------ // Default Comment Provider // ------------------------------------ module.exports = { /** * Init */ async init (config) { WIKI.logger.info('(COMMENTS/DEFAULT) Initializing...') if (WIKI.data.commentProvider.config.akismet && WIKI.data.commentProvider.config.akismet.length > 2) { akismetClient = new AkismetClient({ key: WIKI.data.commentProvider.config.akismet, blog: WIKI.config.host, lang: WIKI.config.lang.namespacing ? WIKI.config.lang.namespaces.join(', ') : WIKI.config.lang.code, charset: 'UTF-8' }) try { const isValid = await akismetClient.verifyKey() if (!isValid) { akismetClient = null WIKI.logger.warn('(COMMENTS/DEFAULT) Akismet Key is invalid! [ DISABLED ]') } else { WIKI.logger.info('(COMMENTS/DEFAULT) Akismet key is valid. [ OK ]') } } catch (err) { akismetClient = null WIKI.logger.warn('(COMMENTS/DEFAULT) Unable to verify Akismet Key: ' + err.message) } } else { akismetClient = null } WIKI.logger.info('(COMMENTS/DEFAULT) Initialization completed.') }, /** * Create New Comment */ async create ({ page, replyTo, content, user }) { // -> Build New Comment const newComment = { content, render: DOMPurify.sanitize(mkdown.render(content)), replyTo, pageId: page.id, authorId: user.id, name: user.name, email: user.email, ip: user.ip } // -> Check for Spam with Akismet if (akismetClient) { let userRole = 'user' if (user.groups.indexOf(1) >= 0) { userRole = 'administrator' } else if (user.groups.indexOf(2) >= 0) { userRole = 'guest' } let isSpam = false try { isSpam = await akismetClient.checkSpam({ ip: user.ip, useragent: user.agentagent, content, name: user.name, email: user.email, permalink: `${WIKI.config.host}/${page.localeCode}/${page.path}`, permalinkDate: page.updatedAt, type: (replyTo > 0) ? 'reply' : 'comment', role: userRole }) } catch (err) { WIKI.logger.warn('Akismet Comment Validation: [ FAILED ]') WIKI.logger.warn(err) } if (isSpam) { throw new Error('Comment was rejected because it is marked as spam.') } } // -> Check for minimum delay between posts if (WIKI.data.commentProvider.config.minDelay > 0) { const lastComment = await WIKI.models.comments.query().select('updatedAt').findOne('authorId', user.id).orderBy('updatedAt', 'desc') if (lastComment && moment().subtract(WIKI.data.commentProvider.config.minDelay, 'seconds').isBefore(lastComment.updatedAt)) { throw new Error('Your administrator has set a time limit before you can post another comment. Try again later.') } } // -> Save Comment to DB const cm = await WIKI.models.comments.query().insert(newComment) // -> Return Comment ID return cm.id }, /** * Update an existing comment */ async update ({ id, content, user }) { const renderedContent = DOMPurify.sanitize(mkdown.render(content)) await WIKI.models.comments.query().findById(id).patch({ content, render: renderedContent }) return renderedContent }, /** * Delete an existing comment by ID */ async remove ({ id, user }) { return WIKI.models.comments.query().findById(id).delete() }, /** * Get the page ID from a comment ID */ async getPageIdFromCommentId (id) { const result = await WIKI.models.comments.query().select('pageId').findById(id) return (result) ? result.pageId : false }, /** * Get a comment by ID */ async getCommentById (id) { return WIKI.models.comments.query().findById(id) }, /** * Get the total comments count for a page ID */ async count (pageId) { const result = await WIKI.models.comments.query().count('* as total').where('pageId', pageId).first() return _.toSafeInteger(result.total) } } ================================================ FILE: server/modules/comments/default/definition.yml ================================================ key: default title: Default description: Built-in advanced comments tool. author: requarks.io logo: https://static.requarks.io/logo/wikijs-butterfly.svg website: https://wiki.js.org codeTemplate: false isAvailable: true props: akismet: type: String title: Akismet API Key default: '' hint: 'Prevent spam by using the Akismet service. Enter your API key here to enable. Leave empty to disable.' maxWidth: 650 order: 1 minDelay: type: Number title: Post delay default: 30 hint: 'Minimum delay (in seconds) between comments per account. Note that all guests are considered as a single account.' maxWidth: 400 order: 2 ================================================ FILE: server/modules/comments/disqus/code.yml ================================================ main: |
body: | ================================================ FILE: server/modules/comments/disqus/definition.yml ================================================ key: disqus title: Disqus description: Disqus help publishers power online discussions with comments. author: requarks.io logo: https://static.requarks.io/logo/disqus.svg website: https://disqus.com/ codeTemplate: true isAvailable: true props: accountName: type: String title: Shortname default: '' hint: Unique identifier from Disqus to identify your website order: 1 ================================================ FILE: server/modules/editor/api/definition.yml ================================================ key: api title: API Docs description: REST / GraphQL Editor contentType: yml author: requarks.io props: {} ================================================ FILE: server/modules/editor/asciidoc/definition.yml ================================================ key: asciidoc title: Asciidoc description: Basic Asciidoc editor contentType: asciidoc author: dzruyk props: {} ================================================ FILE: server/modules/editor/ckeditor/definition.yml ================================================ key: ckeditor title: Visual Editor description: Rich-text WYSIWYG Editor contentType: html author: requarks.io props: {} ================================================ FILE: server/modules/editor/code/definition.yml ================================================ key: code title: Code description: Raw HTML editor contentType: html author: requarks.io props: {} ================================================ FILE: server/modules/editor/markdown/definition.yml ================================================ key: markdown title: Markdown description: Basic Markdown editor contentType: markdown author: requarks.io props: {} ================================================ FILE: server/modules/editor/redirect/definition.yml ================================================ key: redirect title: Redirection description: Redirect the user contentType: redirect author: requarks.io props: {} ================================================ FILE: server/modules/editor/wysiwyg/definition.yml ================================================ key: wysiwyg title: WYSIWYG description: Advanced Visual HTML Builder contentType: html author: requarks.io props: {} ================================================ FILE: server/modules/extensions/git/ext.js ================================================ const cmdExists = require('command-exists') module.exports = { key: 'git', title: 'Git', description: 'Distributed version control system. Required for the Git storage module.', isInstalled: false, async isCompatible () { return true }, async check () { try { await cmdExists('git') this.isInstalled = true } catch (err) { this.isInstalled = false } return this.isInstalled } } ================================================ FILE: server/modules/extensions/pandoc/ext.js ================================================ const cmdExists = require('command-exists') const os = require('os') module.exports = { key: 'pandoc', title: 'Pandoc', description: 'Convert between markup formats. Required for converting from other formats such as MediaWiki, AsciiDoc, Textile and other wikis.', async isCompatible () { return os.arch() === 'x64' }, isInstalled: false, async check () { try { await cmdExists('pandoc') this.isInstalled = true } catch (err) { this.isInstalled = false } return this.isInstalled } } ================================================ FILE: server/modules/extensions/puppeteer/ext.js ================================================ const cmdExists = require('command-exists') const os = require('os') module.exports = { key: 'puppeteer', title: 'Puppeteer', description: 'Headless chromium browser for server-side rendering. Required for generating PDF versions of pages and render content elements on the server (e.g. Mermaid diagrams)', async isCompatible () { return os.arch() === 'x64' }, isInstalled: false, async check () { try { await cmdExists('pandoc') this.isInstalled = true } catch (err) { this.isInstalled = false } return this.isInstalled } } ================================================ FILE: server/modules/extensions/sharp/ext.js ================================================ const fs = require('fs-extra') const os = require('os') const path = require('path') /* global WIKI */ module.exports = { key: 'sharp', title: 'Sharp', description: 'Process and transform images. Required to generate thumbnails of uploaded images and perform transformations.', async isCompatible () { return os.arch() === 'x64' }, isInstalled: false, async check () { this.isInstalled = await fs.pathExists(path.join(WIKI.ROOTPATH, 'node_modules/sharp')) return this.isInstalled } } ================================================ FILE: server/modules/logging/airbrake/definition.yml ================================================ key: airbrake title: Airbrake description: Airbrake is the leading exception reporting service, currently providing error monitoring for 50,000 applications with support for 18 programming languages. author: requarks.io logo: https://static.requarks.io/logo/airbrake.svg website: https://airbrake.io/ defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/airbrake/logger.js ================================================ // ------------------------------------ // Airbrake // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/bugsnag/definition.yml ================================================ key: bugsnag title: Bugsnag description: Bugsnag monitors apps for errors that impact customers & reports all diagnostic data. author: requarks.io logo: https://static.requarks.io/logo/bugsnag.svg website: https://www.bugsnag.com/ defaultLevel: warn props: key: type: String title: Key hint: Bugsnag Project Notifier key ================================================ FILE: server/modules/logging/bugsnag/logger.js ================================================ const util = require('util') const winston = require('winston') const _ = require('lodash') // ------------------------------------ // Bugsnag // ------------------------------------ module.exports = { init (logger, conf) { let BugsnagLogger = winston.transports.BugsnagLogger = function (options) { this.name = 'bugsnagLogger' this.level = options.level || 'warn' this.bugsnag = require('bugsnag') this.bugsnag.register(options.key) } util.inherits(BugsnagLogger, winston.Transport) BugsnagLogger.prototype.log = function (level, msg, meta, callback) { this.bugsnag.notify(new Error(msg), _.assignIn(meta, { severity: level })) callback(null, true) } logger.add(new BugsnagLogger({ level: 'warn', key: conf.key })) } } ================================================ FILE: server/modules/logging/disk/definition.yml ================================================ key: disk title: Log Files description: Outputs log files on local disk. author: requarks.io logo: https://static.requarks.io/logo/local-fs.svg website: https://wiki.js.org defaultLevel: info props: {} ================================================ FILE: server/modules/logging/disk/logger.js ================================================ // ------------------------------------ // Disk // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/eventlog/definition.yml ================================================ key: eventlog title: Windows Event Log description: Report logs to the Windows Event Log author: requarks.io logo: https://static.requarks.io/logo/windows-server.svg website: https://wiki.js.org defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/eventlog/logger.js ================================================ // ------------------------------------ // Windows Event Log // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/loggly/definition.yml ================================================ key: loggly title: Loggly description: Log Analysis / Log Management by Loggly, the world's most popular log analysis & monitoring in the cloud. author: requarks.io logo: https://static.requarks.io/logo/loggly.svg website: https://www.loggly.com/ defaultLevel: warn props: token: type: String title: Token hint: Loggly Token subdomain: type: String title: Subdomain hint: Loggly Subdomain ================================================ FILE: server/modules/logging/loggly/logger.js ================================================ const winston = require('winston') // ------------------------------------ // Loggly // ------------------------------------ module.exports = { init (logger, conf) { require('winston-loggly-bulk') logger.add(new winston.transports.Loggly({ token: conf.token, subdomain: conf.subdomain, tags: ['wiki-js'], level: 'warn', json: true })) } } ================================================ FILE: server/modules/logging/logstash/definition.yml ================================================ key: logstash title: Logstash description: Logstash is an open source tool for collecting, parsing, and storing logs for future use. author: requarks.io logo: https://static.requarks.io/logo/logstash.svg website: https://www.elastic.co/products/logstash defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/logstash/logger.js ================================================ // ------------------------------------ // Logstash // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/newrelic/definition.yml ================================================ key: newrelic title: New Relic description: New Relic's digital intelligence platform lets developers, ops, and tech teams measure and monitor the performance of their applications and infrastructure. author: requarks.io logo: https://static.requarks.io/logo/newrelic.svg website: https://newrelic.com/ defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/newrelic/logger.js ================================================ // ------------------------------------ // New Relic // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/papertrail/definition.yml ================================================ key: papertrail title: Papertrail description: Frustration-free log management. author: requarks.io logo: https://static.requarks.io/logo/papertrail.svg website: https://papertrailapp.com/ defaultLevel: warn props: host: type: String title: Host port: type: Number title: Port ================================================ FILE: server/modules/logging/papertrail/logger.js ================================================ const winston = require('winston') // ------------------------------------ // Papertrail // ------------------------------------ module.exports = { init (logger, conf) { // eslint-disable-next-line no-unused-expressions require('winston-papertrail').Papertrail // NOSONAR logger.add(new winston.transports.Papertrail({ host: conf.host, port: conf.port, level: 'warn', program: 'wiki.js' })) } } ================================================ FILE: server/modules/logging/raygun/definition.yml ================================================ key: raygun title: Raygun description: Error, crash and performance monitoring for software teams. author: requarks.io logo: https://static.requarks.io/logo/raygun.svg website: https://raygun.com/ defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/raygun/logger.js ================================================ // ------------------------------------ // Raygun // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/logging/rollbar/definition.yml ================================================ key: rollbar title: Rollbar description: Rollbar provides real-time error alerting & debugging tools for developers. author: requarks.io logo: https://static.requarks.io/logo/rollbar.svg website: https://rollbar.com/ defaultLevel: warn props: key: type: String title: Key ================================================ FILE: server/modules/logging/rollbar/logger.js ================================================ const util = require('util') const winston = require('winston') const _ = require('lodash') // ------------------------------------ // Rollbar // ------------------------------------ module.exports = { init (logger, conf) { let RollbarLogger = winston.transports.RollbarLogger = function (options) { this.name = 'rollbarLogger' this.level = options.level || 'warn' this.rollbar = require('rollbar') this.rollbar.init(options.key) } util.inherits(RollbarLogger, winston.Transport) RollbarLogger.prototype.log = function (level, msg, meta, callback) { this.rollbar.handleErrorWithPayloadData(new Error(msg), _.assignIn(meta, { level })) callback(null, true) } logger.add(new RollbarLogger({ level: 'warn', key: conf.key })) } } ================================================ FILE: server/modules/logging/sentry/definition.yml ================================================ key: sentry title: Sentry description: Open-source error tracking that helps developers monitor and fix crashes in real time. author: requarks.io logo: https://static.requarks.io/logo/sentry.svg website: https://sentry.io/ defaultLevel: warn props: key: type: String title: Key ================================================ FILE: server/modules/logging/sentry/logger.js ================================================ const util = require('util') const winston = require('winston') // ------------------------------------ // Sentry // ------------------------------------ module.exports = { init (logger, conf) { let SentryLogger = winston.transports.SentryLogger = function (options) { this.name = 'sentryLogger' this.level = options.level || 'warn' this.raven = require('raven') this.raven.config(options.key).install() } util.inherits(SentryLogger, winston.Transport) SentryLogger.prototype.log = function (level, msg, meta, callback) { level = (level === 'warn') ? 'warning' : level this.raven.captureMessage(msg, { level, extra: meta }) callback(null, true) } logger.add(new SentryLogger({ level: 'warn', key: conf.key })) } } ================================================ FILE: server/modules/logging/syslog/definition.yml ================================================ key: syslog title: Syslog description: Syslog is a way for network devices to send event messages to a logging server. author: requarks.io logo: https://static.requarks.io/logo/syslog.svg website: https://wiki.js.org defaultLevel: warn props: {} ================================================ FILE: server/modules/logging/syslog/logger.js ================================================ // ------------------------------------ // Syslog // ------------------------------------ module.exports = { init (logger, conf) { } } ================================================ FILE: server/modules/rendering/asciidoc-core/definition.yml ================================================ key: asciidocCore title: Core description: Basic Asciidoc Parser author: dzruyk (Based on asciidoctor.js renderer) input: asciidoc output: html icon: mdi-sitemap enabledDefault: true props: safeMode: type: String default: server title: Safe Mode hint: Sets the safe mode to use when parsing content to HTML. order: 1 enum: - unsafe - safe - server - secure ================================================ FILE: server/modules/rendering/asciidoc-core/renderer.js ================================================ const asciidoctor = require('asciidoctor')() const cheerio = require('cheerio') module.exports = { async render() { const html = asciidoctor.convert(this.input, { standalone: false, safe: this.config.safeMode, attributes: { showtitle: true, icons: 'font' } }) const $ = cheerio.load(html, { decodeEntities: true }) $('pre.highlight > code.language-diagram').each((i, elm) => { const diagramContent = Buffer.from($(elm).html(), 'base64').toString() $(elm).parent().replaceWith(`
${diagramContent}`)
    })

    return $.html()
  }
}


================================================
FILE: server/modules/rendering/html-asciinema/definition.yml
================================================
key: htmlAsciinema
title: Asciinema
description: Embed asciinema players from compatible links
author: requarks.io
icon: mdi-theater
enabledDefault: false
dependsOn: htmlCore
props: {}


================================================
FILE: server/modules/rendering/html-asciinema/renderer.js
================================================
module.exports = {
  init($, config) {

  }
}


================================================
FILE: server/modules/rendering/html-blockquotes/definition.yml
================================================
key: htmlBlockquotes
title: Blockquotes
description: Parse blockquotes box styling
author: requarks.io
icon: mdi-alpha-t-box-outline
enabledDefault: true
dependsOn: htmlCore
props: {}


================================================
FILE: server/modules/rendering/html-blockquotes/renderer.js
================================================
module.exports = {
  init($, config) {

  }
}


================================================
FILE: server/modules/rendering/html-codehighlighter/definition.yml
================================================
key: htmlCodehighlighter
title: Code Highlighting Post-Processor
description: Syntax detector for programming code
author: requarks.io
icon: mdi-code-braces
enabledDefault: true
dependsOn: htmlCore
step: pre
props: {}


================================================
FILE: server/modules/rendering/html-codehighlighter/renderer.js
================================================
const hljs = require('highlight.js')

module.exports = {
  async init($, config) {
    $('pre > code').each((idx, elm) => {
      const codeClasses = $(elm).attr('class') || ''
      if (codeClasses.indexOf('language-') < 0) {
        const result = hljs.highlightAuto($(elm).text())
        $(elm).addClass('language-', result.language)
      }
      $(elm).parent().addClass('prismjs line-numbers')
    })
  }
}


================================================
FILE: server/modules/rendering/html-core/definition.yml
================================================
key: htmlCore
title: Core
description: Basic HTML Parser
author: requarks.io
input: html
output: html
icon: mdi-language-html5
props:
  absoluteLinks:
    type: Boolean
    default: false
    title: Treat relative links as root absolute
    hint: For example, a link to foo/bar on page xyz will render as /foo/bar instead of /xyz/foo/bar.
    order: 1
  openExternalLinkNewTab:
    type: Boolean
    default: false
    title: Open external links in a new tab
    hint: External links will have a _blank target attribute added automatically.
    order: 2
  relAttributeExternalLink:
    type: String
    default: noreferrer
    title: Protect against XSS when opening _blank target links
    hint: External links with _blank attribute will have an additional rel attribute.
    order: 3
    enum:
        - noreferrer
        - noopener


================================================
FILE: server/modules/rendering/html-core/renderer.js
================================================
const _ = require('lodash')
const cheerio = require('cheerio')
const uslug = require('uslug')
const pageHelper = require('../../../helpers/page')
const URL = require('url').URL

const mustacheRegExp = /(\{|{?){2}(.+?)(\}|}?){2}/i

/* global WIKI */

module.exports = {
  async render() {
    let $ = cheerio.load(this.input, {
      decodeEntities: true
    })

    if ($.root().children().length < 1) {
      return ''
    }

    // --------------------------------
    // STEP: PRE
    // --------------------------------

    for (let child of _.reject(this.children, ['step', 'post'])) {
      const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
      await renderer.init($, child.config)
    }

    // --------------------------------
    // Detect internal / external links
    // --------------------------------

    let internalRefs = []
    const reservedPrefixes = /^\/[a-z]\//i
    const exactReservedPaths = /^\/[a-z]$/i

    const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://'
    if (!isHostSet) {
      WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!')
    }

    $('a').each((i, elm) => {
      let href = $(elm).attr('href')

      // -> Ignore empty / anchor links, e-mail addresses, and telephone numbers
      if (!href || href.length < 1 || href.indexOf('#') === 0 ||
        href.indexOf('mailto:') === 0 || href.indexOf('tel:') === 0) {
        return
      }

      // -> Strip host from local links
      if (isHostSet && href.indexOf(`${WIKI.config.host}/`) === 0) {
        href = href.replace(WIKI.config.host, '')
      }

      // -> Assign local / external tag
      if (href.indexOf('://') < 0) {
        // -> Remove trailing slash
        if (_.endsWith('/')) {
          href = href.slice(0, -1)
        }

        // -> Check for system prefix
        if (reservedPrefixes.test(href) || exactReservedPaths.test(href)) {
          $(elm).addClass(`is-system-link`)
        } else if (href.indexOf('.') >= 0) {
          $(elm).addClass(`is-asset-link`)
        } else {
          let pagePath = null

          // -> Add locale prefix if using namespacing
          if (WIKI.config.lang.namespacing) {
            // -> Reformat paths
            if (href.indexOf('/') !== 0) {
              if (this.config.absoluteLinks) {
                href = `/${this.page.localeCode}/${href}`
              } else {
                href = (this.page.path === 'home') ? `/${this.page.localeCode}/${href}` : `/${this.page.localeCode}/${this.page.path}/${href}`
              }
            } else if (href.charAt(3) !== '/') {
              href = `/${this.page.localeCode}${href}`
            }

            try {
              const parsedUrl = new URL(`http://x${href}`)
              pagePath = pageHelper.parsePath(parsedUrl.pathname)
            } catch (err) {
              return
            }
          } else {
            // -> Reformat paths
            if (href.indexOf('/') !== 0) {
              if (this.config.absoluteLinks) {
                href = `/${href}`
              } else {
                href = (this.page.path === 'home') ? `/${href}` : `/${this.page.path}/${href}`
              }
            }

            try {
              const parsedUrl = new URL(`http://x${href}`)
              pagePath = pageHelper.parsePath(parsedUrl.pathname)
            } catch (err) {
              return
            }
          }
          // -> Save internal references
          internalRefs.push({
            localeCode: pagePath.locale,
            path: pagePath.path
          })

          $(elm).addClass(`is-internal-link`)
        }
      } else {
        $(elm).addClass(`is-external-link`)
        if (this.config.openExternalLinkNewTab) {
          $(elm).attr('target', '_blank')
          $(elm).attr('rel', this.config.relAttributeExternalLink)
        }
      }

      // -> Update element
      $(elm).attr('href', href)
    })

    // --------------------------------
    // Detect internal link states
    // --------------------------------

    const pastLinks = await this.page.$relatedQuery('links')

    if (internalRefs.length > 0) {
      // -> Find matching pages
      const results = await WIKI.models.pages.query().column('id', 'path', 'localeCode').where(builder => {
        internalRefs.forEach((ref, idx) => {
          if (idx < 1) {
            builder.where(ref)
          } else {
            builder.orWhere(ref)
          }
        })
      })

      // -> Apply tag to internal links for found pages
      $('a.is-internal-link').each((i, elm) => {
        const href = $(elm).attr('href')
        let hrefObj = {}
        try {
          const parsedUrl = new URL(`http://x${href}`)
          hrefObj = pageHelper.parsePath(parsedUrl.pathname)
        } catch (err) {
          return
        }
        if (_.some(results, r => {
          return r.localeCode === hrefObj.locale && r.path === hrefObj.path
        })) {
          $(elm).addClass(`is-valid-page`)
        } else {
          $(elm).addClass(`is-invalid-page`)
        }
      })

      // -> Add missing links
      const missingLinks = _.differenceWith(internalRefs, pastLinks, (nLink, pLink) => {
        return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
      })
      if (missingLinks.length > 0) {
        if (WIKI.config.db.type === 'postgres') {
          await WIKI.models.pageLinks.query().insert(missingLinks.map(lnk => ({
            pageId: this.page.id,
            path: lnk.path,
            localeCode: lnk.localeCode
          })))
        } else {
          for (const lnk of missingLinks) {
            await WIKI.models.pageLinks.query().insert({
              pageId: this.page.id,
              path: lnk.path,
              localeCode: lnk.localeCode
            })
          }
        }
      }
    }

    // -> Remove outdated links
    if (pastLinks) {
      const outdatedLinks = _.differenceWith(pastLinks, internalRefs, (nLink, pLink) => {
        return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
      })
      if (outdatedLinks.length > 0) {
        await WIKI.models.pageLinks.query().delete().whereIn('id', _.map(outdatedLinks, 'id'))
      }
    }

    // --------------------------------
    // Add header handles
    // --------------------------------

    let headers = []
    $('h1,h2,h3,h4,h5,h6').each((i, elm) => {
      let headerSlug = uslug($(elm).text())
      // -> If custom ID is defined, try to use that instead
      if ($(elm).attr('id')) {
        headerSlug = $(elm).attr('id')
      }

      // -> Cannot start with a number (CSS selector limitation)
      if (headerSlug.match(/^\d/)) {
        headerSlug = `h-${headerSlug}`
      }

      // -> Make sure header is unique
      if (headers.indexOf(headerSlug) >= 0) {
        let isUnique = false
        let hIdx = 1
        while (!isUnique) {
          const headerSlugTry = `${headerSlug}-${hIdx}`
          if (headers.indexOf(headerSlugTry) < 0) {
            isUnique = true
            headerSlug = headerSlugTry
          }
          hIdx++
        }
      }

      // -> Add anchor
      $(elm).attr('id', headerSlug).addClass('toc-header')
      $(elm).prepend(` `)

      headers.push(headerSlug)
    })

    // --------------------------------
    // Wrap non-empty root text nodes
    // --------------------------------

    $('body').contents().toArray().forEach(item => {
      if (item && item.type === 'text' && item.parent.name === 'body' && item.data !== `\n` && item.data !== `\r`) {
        $(item).wrap('
') } }) // -------------------------------- // Wrap root table nodes // -------------------------------- $('body').contents().toArray().forEach(item => { if (item && item.name === 'table' && item.parent.name === 'body') { $(item).wrap('
') } }) // -------------------------------- // STEP: POST // -------------------------------- let output = decodeEscape($.html('body').replace('', '').replace('', '')) for (let child of _.sortBy(_.filter(this.children, ['step', 'post']), ['order'])) { const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) output = await renderer.init(output, child.config) } // -------------------------------- // Escape mustache expresions // -------------------------------- $ = cheerio.load(output, { decodeEntities: true }) function iterateMustacheNode (node) { $(node).contents().each((idx, item) => { if (item && item.type === 'text') { const rawText = $(item).text().replace(/\r?\n|\r/g, '') if (mustacheRegExp.test(rawText)) { if (!item.parent || item.parent.name === 'body') { $(item).wrap($('

').attr('v-pre', true)) } else { $(item).parent().attr('v-pre', true) } } } else { iterateMustacheNode(item) } }) } iterateMustacheNode($.root()) $('pre').each((idx, elm) => { $(elm).attr('v-pre', true) }) return decodeEscape($.html('body').replace('', '').replace('', '')) } } function decodeEscape (string) { return string.replace(/&#x([0-9a-f]{1,6});/ig, (entity, code) => { code = parseInt(code, 16) // Don't unescape ASCII characters, assuming they're encoded for a good reason if (code < 0x80) return entity return String.fromCodePoint(code) }) } ================================================ FILE: server/modules/rendering/html-diagram/definition.yml ================================================ key: htmlDiagram title: Diagrams Post-Processor description: HTML Processing for diagrams (draw.io) author: requarks.io icon: mdi-chart-multiline enabledDefault: true dependsOn: htmlCore props: {} ================================================ FILE: server/modules/rendering/html-diagram/renderer.js ================================================ module.exports = { async init($, config) { $(`pre.diagram`).each((idx, elm) => { $(elm).children('svg').each((sidx, svg) => { $(svg).removeAttr('content') }) $(elm).replaceWith($(`

${$(elm).html()}
`)) }) } } ================================================ FILE: server/modules/rendering/html-image-prefetch/definition.yml ================================================ key: htmlImagePrefetch title: Image Prefetch description: Prefetch remotely rendered images (kroki/plantuml) author: requarks.io icon: mdi-cloud-download-outline enabledDefault: false dependsOn: htmlCore props: {} ================================================ FILE: server/modules/rendering/html-image-prefetch/renderer.js ================================================ const request = require('request-promise') const prefetch = async (element) => { const url = element.attr(`src`) let response try { response = await request({ method: `GET`, url, resolveWithFullResponse: true }) } catch (err) { WIKI.logger.warn(`Failed to prefetch ${url}`) WIKI.logger.warn(err) return } const contentType = response.headers[`content-type`] const image = Buffer.from(response.body).toString('base64') element.attr('src', `data:${contentType};base64,${image}`) element.removeClass('prefetch-candidate') } module.exports = { async init($) { const promises = $('img.prefetch-candidate').map((index, element) => { return prefetch($(element)) }).toArray() await Promise.all(promises) } } ================================================ FILE: server/modules/rendering/html-mediaplayers/definition.yml ================================================ key: htmlMediaplayers title: Media Players description: Embed players such as Youtube, Vimeo, Soundcloud, etc. author: requarks.io icon: mdi-video enabledDefault: true dependsOn: htmlCore props: {} ================================================ FILE: server/modules/rendering/html-mediaplayers/renderer.js ================================================ module.exports = { init($, config) { } } ================================================ FILE: server/modules/rendering/html-mermaid/definition.yml ================================================ key: htmlMermaid title: Mermaid description: Generate flowcharts from Mermaid syntax author: requarks.io icon: mdi-arrow-decision-outline enabledDefault: true dependsOn: htmlCore props: {} ================================================ FILE: server/modules/rendering/html-mermaid/renderer.js ================================================ module.exports = { init($, config) { $('pre.prismjs > code.language-mermaid').each((i, elm) => { const mermaidContent = $(elm).html() $(elm).parent().replaceWith(`
${mermaidContent}
`) }) } } ================================================ FILE: server/modules/rendering/html-security/definition.yml ================================================ key: htmlSecurity title: Security description: Filter and strips potentially dangerous content author: requarks.io icon: mdi-fire enabledDefault: true dependsOn: htmlCore step: post order: 99999 props: safeHTML: type: Boolean title: Sanitize HTML default: true hint: Sanitize HTML from unsafe attributes and tags that could lead to XSS attacks order: 1 allowDrawIoUnsafe: type: Boolean title: Allow Draw.io Unsafe Elements default: true hint: Draw.io diagrams may introduce some elements that are usually filtered. Turning off this option may cause some diagrams to be completely removed during the sanitization process. order: 2 allowIFrames: type: Boolean title: Allow iframes default: false hint: iframes will not be stripped if enabled. (Not recommended) order: 3 ================================================ FILE: server/modules/rendering/html-security/renderer.js ================================================ const { JSDOM } = require('jsdom') const createDOMPurify = require('dompurify') module.exports = { async init(input, config) { if (config.safeHTML) { const window = new JSDOM('').window const DOMPurify = createDOMPurify(window) const allowedAttrs = ['v-pre', 'v-slot:tabs', 'v-slot:content', 'target'] const allowedTags = ['tabset', 'template'] if (config.allowDrawIoUnsafe) { allowedTags.push('foreignObject') DOMPurify.addHook('uponSanitizeElement', (elm) => { if (elm.querySelectorAll) { const breaks = elm.querySelectorAll('foreignObject br, foreignObject p') if (breaks && breaks.length) { for (let i = 0; i < breaks.length; i++) { breaks[i].parentNode.replaceChild( window.document.createElement('div'), breaks[i] ) } } } }) } if (config.allowIFrames) { allowedTags.push('iframe') allowedAttrs.push('allow') } input = DOMPurify.sanitize(input, { ADD_ATTR: allowedAttrs, ADD_TAGS: allowedTags, HTML_INTEGRATION_POINTS: { foreignobject: true } }) } return input } } ================================================ FILE: server/modules/rendering/html-tabset/definition.yml ================================================ key: htmlTabset title: Tabsets description: Transform headers into tabs author: requarks.io icon: mdi-tab enabledDefault: true dependsOn: htmlCore props: {} ================================================ FILE: server/modules/rendering/html-tabset/renderer.js ================================================ const _ = require('lodash') module.exports = { async init($, config) { for (let i = 1; i < 6; i++) { $(`h${i}.tabset`).each((idx, elm) => { let content = `` let tabs = [] let tabContents = [] $(elm).nextUntil(_.times(i, t => `h${t + 1}`).join(', '), `h${i + 1}`).each((hidx, hd) => { tabs.push(`
  • ${$(hd).html()}
  • `) let tabContent = '' $(hd).nextUntil(_.times(i + 1, t => `h${t + 1}`).join(', ')).each((cidx, celm) => { tabContent += $.html(celm) $(celm).remove() }) tabContents.push(`
    ${tabContent}
    `) $(hd).remove() }) content += `` content += `` content += `
    ` $(elm).replaceWith($(content)) }) } } } ================================================ FILE: server/modules/rendering/html-twemoji/definition.yml ================================================ key: htmlTwemoji title: Twemoji description: Apply Twitter Emojis to all Unicode emojis author: requarks.io icon: mdi-emoticon-happy-outline enabledDefault: true dependsOn: htmlCore step: post order: 10 props: {} ================================================ FILE: server/modules/rendering/html-twemoji/renderer.js ================================================ // const twemoji = require('twemoji') // ------------------------------------ // HTML - Twemoji // ------------------------------------ module.exports = { init (input, conf) { // TODO: Must limit to text nodes only (exclude code blocks, already processed emojis, etc.) // // return twemoji.parse(input) return input } } ================================================ FILE: server/modules/rendering/markdown-abbr/definition.yml ================================================ key: markdownAbbr title: Abbreviations description: Parse abbreviations into abbr tags author: requarks.io icon: mdi-contain-start enabledDefault: true dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-abbr/renderer.js ================================================ const mdAbbr = require('markdown-it-abbr') // ------------------------------------ // Markdown - Abbreviations // ------------------------------------ module.exports = { init (md, conf) { md.use(mdAbbr) } } ================================================ FILE: server/modules/rendering/markdown-core/definition.yml ================================================ key: markdownCore title: Core description: Basic Markdown Parser author: requarks.io input: markdown output: html icon: mdi-language-markdown props: allowHTML: type: Boolean default: true title: Allow HTML hint: Enable HTML tags in content. order: 1 public: true linkify: type: Boolean default: true title: Automatically convert links hint: Links will automatically be converted to clickable links. order: 2 public: true linebreaks: type: Boolean default: true title: Automatically convert line breaks hint: Add linebreaks within paragraphs. order: 3 public: true underline: type: Boolean default: false title: Underline Emphasis hint: Enable text underlining by using _underline_ syntax. order: 4 public: true typographer: type: Boolean default: false title: Typographer hint: Enable some language-neutral replacement + quotes beautification. order: 5 public: true quotes: type: String default: English title: Quotes style hint: When typographer is enabled. Double + single quotes replacement pairs. e.g. «»„“ for Russian, „“‚‘ for German, etc. order: 6 enum: - Chinese - English - French - German - Greek - Japanese - Hungarian - Polish - Portuguese - Russian - Spanish - Swedish public: true ================================================ FILE: server/modules/rendering/markdown-core/renderer.js ================================================ const md = require('markdown-it') const mdAttrs = require('markdown-it-attrs') const mdDecorate = require('markdown-it-decorate') const _ = require('lodash') const underline = require('./underline') const quoteStyles = { Chinese: '””‘’', English: '“”‘’', French: ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'], German: '„“‚‘', Greek: '«»‘’', Japanese: '「」「」', Hungarian: '„”’’', Polish: '„”‚‘', Portuguese: '«»‘’', Russian: '«»„“', Spanish: '«»‘’', Swedish: '””’’' } module.exports = { async render() { const mkdown = md({ html: this.config.allowHTML, breaks: this.config.linebreaks, linkify: this.config.linkify, typographer: this.config.typographer, quotes: _.get(quoteStyles, this.config.quotes, quoteStyles.English), highlight(str, lang) { if (lang === 'diagram') { return `
    ` + Buffer.from(str, 'base64').toString() + `
    ` } else { return `
    ${_.escape(str)}
    ` } } }) if (this.config.underline) { mkdown.use(underline) } mkdown.use(mdAttrs, { allowedAttributes: ['id', 'class', 'target'] }) mkdown.use(mdDecorate) for (let child of this.children) { const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) await renderer.init(mkdown, child.config) } return mkdown.render(this.input) } } ================================================ FILE: server/modules/rendering/markdown-core/underline.js ================================================ const renderEm = (tokens, idx, opts, env, slf) => { const token = tokens[idx] if (token.markup === '_') { token.tag = 'u' } return slf.renderToken(tokens, idx, opts) } module.exports = (md) => { md.renderer.rules.em_open = renderEm md.renderer.rules.em_close = renderEm } ================================================ FILE: server/modules/rendering/markdown-emoji/definition.yml ================================================ key: markdownEmoji title: Emoji description: Convert tags to emojis author: requarks.io icon: mdi-sticker-emoji enabledDefault: true dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-emoji/renderer.js ================================================ const { full: mdEmoji } = require('markdown-it-emoji') const twemoji = require('twemoji') // ------------------------------------ // Markdown - Emoji // ------------------------------------ module.exports = { init (md, conf) { md.use(mdEmoji) md.renderer.rules.emoji = (token, idx) => { return twemoji.parse(token[idx].content, { callback (icon, opts) { return `/_assets/svg/twemoji/${icon}.svg` } }) } } } ================================================ FILE: server/modules/rendering/markdown-expandtabs/definition.yml ================================================ key: markdownExpandtabs title: Expand Tabs description: Replace tabs with spaces in code blocks author: requarks.io icon: mdi-arrow-expand-horizontal enabledDefault: true dependsOn: markdownCore props: tabWidth: type: Number title: Tab Width hint: Amount of spaces for each tab default: 4 ================================================ FILE: server/modules/rendering/markdown-expandtabs/renderer.js ================================================ const mdExpandTabs = require('markdown-it-expand-tabs') const _ = require('lodash') // ------------------------------------ // Markdown - Expand Tabs // ------------------------------------ module.exports = { init (md, conf) { md.use(mdExpandTabs, { tabWidth: _.toInteger(conf.tabWidth || 4) }) } } ================================================ FILE: server/modules/rendering/markdown-footnotes/definition.yml ================================================ key: markdownFootnotes title: Footnotes description: Parse footnotes references author: requarks.io icon: mdi-page-layout-footer enabledDefault: true dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-footnotes/renderer.js ================================================ const mdFootnote = require('markdown-it-footnote') // ------------------------------------ // Markdown - Footnotes // ------------------------------------ module.exports = { init (md, conf) { md.use(mdFootnote) } } ================================================ FILE: server/modules/rendering/markdown-imsize/definition.yml ================================================ key: markdownImsize title: Image Size description: Adds dimensions attributes to images author: requarks.io icon: mdi-image-size-select-large enabledDefault: true dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-imsize/renderer.js ================================================ const mdImsize = require('markdown-it-imsize') // ------------------------------------ // Markdown - Image Size // ------------------------------------ module.exports = { init (md, conf) { md.use(mdImsize) } } ================================================ FILE: server/modules/rendering/markdown-katex/definition.yml ================================================ key: markdownKatex title: Katex description: LaTeX Math + Chemical Expression Typesetting Renderer author: requarks.io icon: mdi-math-integral enabledDefault: true dependsOn: markdownCore props: useInline: type: Boolean default: true title: Inline TeX hint: Process inline TeX expressions surrounded by $ symbols. order: 1 useBlocks: type: Boolean default: true title: TeX Blocks hint: Process TeX blocks enclosed by $$ symbols. order: 2 ================================================ FILE: server/modules/rendering/markdown-katex/mhchem.js ================================================ /* eslint-disable */ /* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /************************************************************* * * KaTeX mhchem.js * * This file implements a KaTeX version of mhchem version 3.3.0. * It is adapted from MathJax/extensions/TeX/mhchem.js * It differs from the MathJax version as follows: * 1. The interface is changed so that it can be called from KaTeX, not MathJax. * 2. \rlap and \llap are replaced with \mathrlap and \mathllap. * 3. Four lines of code are edited in order to use \raisebox instead of \raise. * 4. The reaction arrow code is simplified. All reaction arrows are rendered * using KaTeX extensible arrows instead of building non-extensible arrows. * 5. \tripledash vertical alignment is slightly adjusted. * * This code, as other KaTeX code, is released under the MIT license. * * /************************************************************* * * MathJax/extensions/TeX/mhchem.js * * Implements the \ce command for handling chemical formulas * from the mhchem LaTeX package. * * --------------------------------------------------------------------- * * Copyright (c) 2011-2015 The MathJax Consortium * Copyright (c) 2015-2018 Martin Hensel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // // Coding Style // - use '' for identifiers that can by minified/uglified // - use "" for strings that need to stay untouched // version: "3.3.0" for MathJax and KaTeX // // This is the main function for handing the \ce and \pu commands. // It takes the argument to \ce or \pu and returns the corresponding TeX string. // module.exports = function (tokens, stateMachine) { // Recreate the argument string from KaTeX's array of tokens. var str = ""; var expectedLoc = tokens[tokens.length - 1].loc.start for (var i = tokens.length - 1; i >= 0; i--) { if(tokens[i].loc.start > expectedLoc) { // context.consumeArgs has eaten a space. str += " "; expectedLoc = tokens[i].loc.start; } str += tokens[i].text; expectedLoc += tokens[i].text.length; } var tex = texify.go(mhchemParser.go(str, stateMachine)); return tex; }; // // Core parser for mhchem syntax (recursive) // /** @type {MhchemParser} */ var mhchemParser = { // // Parses mchem \ce syntax // // Call like // go("H2O"); // go: function (input, stateMachine) { if (!input) { return []; } if (stateMachine === undefined) { stateMachine = 'ce'; } var state = '0'; // // String buffers for parsing: // // buffer.a == amount // buffer.o == element // buffer.b == left-side superscript // buffer.p == left-side subscript // buffer.q == right-side subscript // buffer.d == right-side superscript // // buffer.r == arrow // buffer.rdt == arrow, script above, type // buffer.rd == arrow, script above, content // buffer.rqt == arrow, script below, type // buffer.rq == arrow, script below, content // // buffer.text_ // buffer.rm // etc. // // buffer.parenthesisLevel == int, starting at 0 // buffer.sb == bool, space before // buffer.beginsWithBond == bool // // These letters are also used as state names. // // Other states: // 0 == begin of main part (arrow/operator unlikely) // 1 == next entity // 2 == next entity (arrow/operator unlikely) // 3 == next atom // c == macro // /** @type {Buffer} */ var buffer = {}; buffer['parenthesisLevel'] = 0; input = input.replace(/\n/g, " "); input = input.replace(/[\u2212\u2013\u2014\u2010]/g, "-"); input = input.replace(/[\u2026]/g, "..."); // // Looks through mhchemParser.transitions, to execute a matching action // (recursive) // var lastInput; var watchdog = 10; /** @type {ParserOutput[]} */ var output = []; while (true) { if (lastInput !== input) { watchdog = 10; lastInput = input; } else { watchdog--; } // // Find actions in transition table // var machine = mhchemParser.stateMachines[stateMachine]; var t = machine.transitions[state] || machine.transitions['*']; iterateTransitions: for (var i=0; i 0) { if (!task.revisit) { input = matches.remainder; } if (!task.toContinue) { break iterateTransitions; } } else { return output; } } } // // Prevent infinite loop // if (watchdog <= 0) { throw ["MhchemBugU", "mhchem bug U. Please report."]; // Unexpected character } } }, concatArray: function (a, b) { if (b) { if (Array.isArray(b)) { for (var iB=0; iB': /^[=<>]/, '#': /^[#\u2261]/, '+': /^\+/, '-$': /^-(?=[\s_},;\]/]|$|\([a-z]+\))/, // -space -, -; -] -/ -$ -state-of-aggregation '-9': /^-(?=[0-9])/, '- orbital overlap': /^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/, '-': /^-/, 'pm-operator': /^(?:\\pm|\$\\pm\$|\+-|\+\/-)/, 'operator': /^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/, 'arrowUpDown': /^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/, '\\bond{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\bond{", "", "", "}"); }, '->': /^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/, 'CMT': /^[CMT](?=\[)/, '[(...)]': function (input) { return mhchemParser.patterns.findObserveGroups(input, "[", "", "", "]"); }, '1st-level escape': /^(&|\\\\|\\hline)\s*/, '\\,': /^(?:\\[,\ ;:])/, // \\x - but output no space before '\\x{}{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); }, '\\x{}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", ""); }, '\\ca': /^\\ca(?:\s+|(?![a-zA-Z]))/, '\\x': /^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/, 'orbital': /^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/, // only those with numbers in front, because the others will be formatted correctly anyway 'others': /^[\/~|]/, '\\frac{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\frac{", "", "", "}", "{", "", "", "}"); }, '\\overset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\overset{", "", "", "}", "{", "", "", "}"); }, '\\underset{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\underset{", "", "", "}", "{", "", "", "}"); }, '\\underbrace{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\underbrace{", "", "", "}_", "{", "", "", "}"); }, '\\color{(...)}0': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}"); }, '\\color{(...)}{(...)}1': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}", "{", "", "", "}"); }, '\\color(...){(...)}2': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\color", "\\", "", /^(?=\{)/, "{", "", "", "}"); }, '\\ce{(...)}': function (input) { return mhchemParser.patterns.findObserveGroups(input, "\\ce{", "", "", "}"); }, 'oxidation$': /^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, 'd-oxidation$': /^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, // 0 could be oxidation or charge 'roman numeral': /^[IVX]+/, '1/2$': /^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/, 'amount': function (input) { var match; // e.g. 2, 0.5, 1/2, -2, n/2, +; $a$ could be added later in parsing match = input.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/); if (match) { return { match_: match[0], remainder: input.substr(match[0].length) }; } var a = mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); if (a) { // e.g. $2n-1$, $-$ match = a.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/); if (match) { return { match_: match[0], remainder: input.substr(match[0].length) }; } } return null; }, 'amount2': function (input) { return this['amount'](input); }, '(KV letters),': /^(?:[A-Z][a-z]{0,2}|i)(?=,)/, 'formula$': function (input) { if (input.match(/^\([a-z]+\)$/)) { return null; } // state of aggregation = no formula var match = input.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/); if (match) { return { match_: match[0], remainder: input.substr(match[0].length) }; } return null; }, 'uprightEntities': /^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/, '/': /^\s*(\/)\s*/, '//': /^\s*(\/\/)\s*/, '*': /^\s*[*.]\s*/ }, findObserveGroups: function (input, begExcl, begIncl, endIncl, endExcl, beg2Excl, beg2Incl, end2Incl, end2Excl, combine) { /** @type {{(input: string, pattern: string | RegExp): string | string[] | null;}} */ var _match = function (input, pattern) { if (typeof pattern === "string") { if (input.indexOf(pattern) !== 0) { return null; } return pattern; } else { var match = input.match(pattern); if (!match) { return null; } return match[0]; } }; /** @type {{(input: string, i: number, endChars: string | RegExp): {endMatchBegin: number, endMatchEnd: number} | null;}} */ var _findObserveGroups = function (input, i, endChars) { var braces = 0; while (i < input.length) { var a = input.charAt(i); var match = _match(input.substr(i), endChars); if (match !== null && braces === 0) { return { endMatchBegin: i, endMatchEnd: i + match.length }; } else if (a === "{") { braces++; } else if (a === "}") { if (braces === 0) { throw ["ExtraCloseMissingOpen", "Extra close brace or missing open brace"]; } else { braces--; } } i++; } if (braces > 0) { return null; } return null; }; var match = _match(input, begExcl); if (match === null) { return null; } input = input.substr(match.length); match = _match(input, begIncl); if (match === null) { return null; } var e = _findObserveGroups(input, match.length, endIncl || endExcl); if (e === null) { return null; } var match1 = input.substring(0, (endIncl ? e.endMatchEnd : e.endMatchBegin)); if (!(beg2Excl || beg2Incl)) { return { match_: match1, remainder: input.substr(e.endMatchEnd) }; } else { var group2 = this.findObserveGroups(input.substr(e.endMatchEnd), beg2Excl, beg2Incl, end2Incl, end2Excl); if (group2 === null) { return null; } /** @type {string[]} */ var matchRet = [match1, group2.match_]; return { match_: (combine ? matchRet.join("") : matchRet), remainder: group2.remainder }; } }, // // Matching function // e.g. match("a", input) will look for the regexp called "a" and see if it matches // returns null or {match_:"a", remainder:"bc"} // match_: function (m, input) { var pattern = mhchemParser.patterns.patterns[m]; if (pattern === undefined) { throw ["MhchemBugP", "mhchem bug P. Please report. (" + m + ")"]; // Trying to use non-existing pattern } else if (typeof pattern === "function") { return mhchemParser.patterns.patterns[m](input); // cannot use cached var pattern here, because some pattern functions need this===mhchemParser } else { // RegExp var match = input.match(pattern); if (match) { var mm; if (match[2]) { mm = [ match[1], match[2] ]; } else if (match[1]) { mm = match[1]; } else { mm = match[0]; } return { match_: mm, remainder: input.substr(match[0].length) }; } return null; } } }, // // Generic state machine actions // actions: { 'a=': function (buffer, m) { buffer.a = (buffer.a || "") + m; }, 'b=': function (buffer, m) { buffer.b = (buffer.b || "") + m; }, 'p=': function (buffer, m) { buffer.p = (buffer.p || "") + m; }, 'o=': function (buffer, m) { buffer.o = (buffer.o || "") + m; }, 'q=': function (buffer, m) { buffer.q = (buffer.q || "") + m; }, 'd=': function (buffer, m) { buffer.d = (buffer.d || "") + m; }, 'rm=': function (buffer, m) { buffer.rm = (buffer.rm || "") + m; }, 'text=': function (buffer, m) { buffer.text_ = (buffer.text_ || "") + m; }, 'insert': function (buffer, m, a) { return { type_: a }; }, 'insert+p1': function (buffer, m, a) { return { type_: a, p1: m }; }, 'insert+p1+p2': function (buffer, m, a) { return { type_: a, p1: m[0], p2: m[1] }; }, 'copy': function (buffer, m) { return m; }, 'rm': function (buffer, m) { return { type_: 'rm', p1: m || ""}; }, 'text': function (buffer, m) { return mhchemParser.go(m, 'text'); }, '{text}': function (buffer, m) { var ret = [ "{" ]; mhchemParser.concatArray(ret, mhchemParser.go(m, 'text')); ret.push("}"); return ret; }, 'tex-math': function (buffer, m) { return mhchemParser.go(m, 'tex-math'); }, 'tex-math tight': function (buffer, m) { return mhchemParser.go(m, 'tex-math tight'); }, 'bond': function (buffer, m, k) { return { type_: 'bond', kind_: k || m }; }, 'color0-output': function (buffer, m) { return { type_: 'color0', color: m[0] }; }, 'ce': function (buffer, m) { return mhchemParser.go(m); }, '1/2': function (buffer, m) { /** @type {ParserOutput[]} */ var ret = []; if (m.match(/^[+\-]/)) { ret.push(m.substr(0, 1)); m = m.substr(1); } var n = m.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/); n[1] = n[1].replace(/\$/g, ""); ret.push({ type_: 'frac', p1: n[1], p2: n[2] }); if (n[3]) { n[3] = n[3].replace(/\$/g, ""); ret.push({ type_: 'tex-math', p1: n[3] }); } return ret; }, '9,9': function (buffer, m) { return mhchemParser.go(m, '9,9'); } }, // // createTransitions // convert { 'letter': { 'state': { action_: 'output' } } } to { 'state' => [ { pattern: 'letter', task: { action_: [{type_: 'output'}] } } ] } // with expansion of 'a|b' to 'a' and 'b' (at 2 places) // createTransitions: function (o) { var pattern, state; /** @type {string[]} */ var stateArray; var i; // // 1. Collect all states // /** @type {Transitions} */ var transitions = {}; for (pattern in o) { for (state in o[pattern]) { stateArray = state.split("|"); o[pattern][state].stateArray = stateArray; for (i=0; i': { '0|1|2|3': { action_: 'r=', nextState: 'r' }, 'a|as': { action_: [ 'output', 'r=' ], nextState: 'r' }, '*': { action_: [ 'output', 'r=' ], nextState: 'r' } }, '+': { 'o': { action_: 'd= kv', nextState: 'd' }, 'd|D': { action_: 'd=', nextState: 'd' }, 'q': { action_: 'd=', nextState: 'qd' }, 'qd|qD': { action_: 'd=', nextState: 'qd' }, 'dq': { action_: [ 'output', 'd=' ], nextState: 'd' }, '3': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } }, 'amount': { '0|2': { action_: 'a=', nextState: 'a' } }, 'pm-operator': { '0|1|2|a|as': { action_: [ 'sb=false', 'output', { type_: 'operator', option: '\\pm' } ], nextState: '0' } }, 'operator': { '0|1|2|a|as': { action_: [ 'sb=false', 'output', 'operator' ], nextState: '0' } }, '-$': { 'o|q': { action_: [ 'charge or bond', 'output' ], nextState: 'qd' }, 'd': { action_: 'd=', nextState: 'd' }, 'D': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' }, 'q': { action_: 'd=', nextState: 'qd' }, 'qd': { action_: 'd=', nextState: 'qd' }, 'qD|dq': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' } }, '-9': { '3|o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '3' } }, '- orbital overlap': { 'o': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' }, 'd': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' } }, '-': { '0|1|2': { action_: [ { type_: 'output', option: 1 }, 'beginsWithBond=true', { type_: 'bond', option: "-" } ], nextState: '3' }, '3': { action_: { type_: 'bond', option: "-" } }, 'a': { action_: [ 'output', { type_: 'insert', option: 'hyphen' } ], nextState: '2' }, 'as': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: "-" } ], nextState: '3' }, 'b': { action_: 'b=' }, 'o': { action_: { type_: '- after o/d', option: false }, nextState: '2' }, 'q': { action_: { type_: '- after o/d', option: false }, nextState: '2' }, 'd|qd|dq': { action_: { type_: '- after o/d', option: true }, nextState: '2' }, 'D|qD|p': { action_: [ 'output', { type_: 'bond', option: "-" } ], nextState: '3' } }, 'amount2': { '1|3': { action_: 'a=', nextState: 'a' } }, 'letters': { '0|1|2|3|a|as|b|p|bp|o': { action_: 'o=', nextState: 'o' }, 'q|dq': { action_: ['output', 'o='], nextState: 'o' }, 'd|D|qd|qD': { action_: 'o after d', nextState: 'o' } }, 'digits': { 'o': { action_: 'q=', nextState: 'q' }, 'd|D': { action_: 'q=', nextState: 'dq' }, 'q': { action_: [ 'output', 'o=' ], nextState: 'o' }, 'a': { action_: 'o=', nextState: 'o' } }, 'space A': { 'b|p|bp': {} }, 'space': { 'a': { nextState: 'as' }, '0': { action_: 'sb=false' }, '1|2': { action_: 'sb=true' }, 'r|rt|rd|rdt|rdq': { action_: 'output', nextState: '0' }, '*': { action_: [ 'output', 'sb=true' ], nextState: '1'} }, '1st-level escape': { '1|2': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ] }, '*': { action_: [ 'output', { type_: 'insert+p1', option: '1st-level escape' } ], nextState: '0' } }, '[(...)]': { 'r|rt': { action_: 'rd=', nextState: 'rd' }, 'rd|rdt': { action_: 'rq=', nextState: 'rdq' } }, '...': { 'o|d|D|dq|qd|qD': { action_: [ 'output', { type_: 'bond', option: "..." } ], nextState: '3' }, '*': { action_: [ { type_: 'output', option: 1 }, { type_: 'insert', option: 'ellipsis' } ], nextState: '1' } }, '. |* ': { '*': { action_: [ 'output', { type_: 'insert', option: 'addition compound' } ], nextState: '1' } }, 'state of aggregation $': { '*': { action_: [ 'output', 'state of aggregation' ], nextState: '1' } }, '{[(': { 'a|as|o': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' }, '0|1|2|3': { action_: [ 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' }, '*': { action_: [ 'output', 'o=', 'output', 'parenthesisLevel++' ], nextState: '2' } }, ')]}': { '0|1|2|3|b|p|bp|o': { action_: [ 'o=', 'parenthesisLevel--' ], nextState: 'o' }, 'a|as|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=', 'parenthesisLevel--' ], nextState: 'o' } }, ', ': { '*': { action_: [ 'output', 'comma' ], nextState: '0' } }, '^_': { // ^ and _ without a sensible argument '*': { } }, '^{(...)}|^($...$)': { '0|1|2|as': { action_: 'b=', nextState: 'b' }, 'p': { action_: 'b=', nextState: 'bp' }, '3|o': { action_: 'd= kv', nextState: 'D' }, 'q': { action_: 'd=', nextState: 'qD' }, 'd|D|qd|qD|dq': { action_: [ 'output', 'd=' ], nextState: 'D' } }, '^a|^\\x{}{}|^\\x{}|^\\x|\'': { '0|1|2|as': { action_: 'b=', nextState: 'b' }, 'p': { action_: 'b=', nextState: 'bp' }, '3|o': { action_: 'd= kv', nextState: 'd' }, 'q': { action_: 'd=', nextState: 'qd' }, 'd|qd|D|qD': { action_: 'd=' }, 'dq': { action_: [ 'output', 'd=' ], nextState: 'd' } }, '_{(state of aggregation)}$': { 'd|D|q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } }, '_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x': { '0|1|2|as': { action_: 'p=', nextState: 'p' }, 'b': { action_: 'p=', nextState: 'bp' }, '3|o': { action_: 'q=', nextState: 'q' }, 'd|D': { action_: 'q=', nextState: 'dq' }, 'q|qd|qD|dq': { action_: [ 'output', 'q=' ], nextState: 'q' } }, '=<>': { '0|1|2|3|a|as|o|q|d|D|qd|qD|dq': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: '3' } }, '#': { '0|1|2|3|a|as|o': { action_: [ { type_: 'output', option: 2 }, { type_: 'bond', option: "#" } ], nextState: '3' } }, '{}': { '*': { action_: { type_: 'output', option: 1 }, nextState: '1' } }, '{...}': { '0|1|2|3|a|as|b|p|bp': { action_: 'o=', nextState: 'o' }, 'o|d|D|q|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } }, '$...$': { 'a': { action_: 'a=' }, // 2$n$ '0|1|2|3|as|b|p|bp|o': { action_: 'o=', nextState: 'o' }, // not 'amount' 'as|o': { action_: 'o=' }, 'q|d|D|qd|qD|dq': { action_: [ 'output', 'o=' ], nextState: 'o' } }, '\\bond{(...)}': { '*': { action_: [ { type_: 'output', option: 2 }, 'bond' ], nextState: "3" } }, '\\frac{(...)}': { '*': { action_: [ { type_: 'output', option: 1 }, 'frac-output' ], nextState: '3' } }, '\\overset{(...)}': { '*': { action_: [ { type_: 'output', option: 2 }, 'overset-output' ], nextState: '3' } }, '\\underset{(...)}': { '*': { action_: [ { type_: 'output', option: 2 }, 'underset-output' ], nextState: '3' } }, '\\underbrace{(...)}': { '*': { action_: [ { type_: 'output', option: 2 }, 'underbrace-output' ], nextState: '3' } }, '\\color{(...)}{(...)}1|\\color(...){(...)}2': { '*': { action_: [ { type_: 'output', option: 2 }, 'color-output' ], nextState: '3' } }, '\\color{(...)}0': { '*': { action_: [ { type_: 'output', option: 2 }, 'color0-output' ] } }, '\\ce{(...)}': { '*': { action_: [ { type_: 'output', option: 2 }, 'ce' ], nextState: '3' } }, '\\,': { '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '1' } }, '\\x{}{}|\\x{}|\\x': { '0|1|2|3|a|as|b|p|bp|o|c0': { action_: [ 'o=', 'output' ], nextState: '3' }, '*': { action_: ['output', 'o=', 'output' ], nextState: '3' } }, 'others': { '*': { action_: [ { type_: 'output', option: 1 }, 'copy' ], nextState: '3' } }, 'else2': { 'a': { action_: 'a to o', nextState: 'o', revisit: true }, 'as': { action_: [ 'output', 'sb=true' ], nextState: '1', revisit: true }, 'r|rt|rd|rdt|rdq': { action_: [ 'output' ], nextState: '0', revisit: true }, '*': { action_: [ 'output', 'copy' ], nextState: '3' } } }), actions: { 'o after d': function (buffer, m) { var ret; if ((buffer.d || "").match(/^[0-9]+$/)) { var tmp = buffer.d; buffer.d = undefined; ret = this['output'](buffer); buffer.b = tmp; } else { ret = this['output'](buffer); } mhchemParser.actions['o='](buffer, m); return ret; }, 'd= kv': function (buffer, m) { buffer.d = m; buffer.dType = 'kv'; }, 'charge or bond': function (buffer, m) { if (buffer['beginsWithBond']) { /** @type {ParserOutput[]} */ var ret = []; mhchemParser.concatArray(ret, this['output'](buffer)); mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); return ret; } else { buffer.d = m; } }, '- after o/d': function (buffer, m, isAfterD) { var c1 = mhchemParser.patterns.match_('orbital', buffer.o || ""); var c2 = mhchemParser.patterns.match_('one lowercase greek letter $', buffer.o || ""); var c3 = mhchemParser.patterns.match_('one lowercase latin letter $', buffer.o || ""); var c4 = mhchemParser.patterns.match_('$one lowercase latin letter$ $', buffer.o || ""); var hyphenFollows = m==="-" && ( c1 && c1.remainder==="" || c2 || c3 || c4 ); if (hyphenFollows && !buffer.a && !buffer.b && !buffer.p && !buffer.d && !buffer.q && !c1 && c3) { buffer.o = '$' + buffer.o + '$'; } /** @type {ParserOutput[]} */ var ret = []; if (hyphenFollows) { mhchemParser.concatArray(ret, this['output'](buffer)); ret.push({ type_: 'hyphen' }); } else { c1 = mhchemParser.patterns.match_('digits', buffer.d || ""); if (isAfterD && c1 && c1.remainder==='') { mhchemParser.concatArray(ret, mhchemParser.actions['d='](buffer, m)); mhchemParser.concatArray(ret, this['output'](buffer)); } else { mhchemParser.concatArray(ret, this['output'](buffer)); mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); } } return ret; }, 'a to o': function (buffer) { buffer.o = buffer.a; buffer.a = undefined; }, 'sb=true': function (buffer) { buffer.sb = true; }, 'sb=false': function (buffer) { buffer.sb = false; }, 'beginsWithBond=true': function (buffer) { buffer['beginsWithBond'] = true; }, 'beginsWithBond=false': function (buffer) { buffer['beginsWithBond'] = false; }, 'parenthesisLevel++': function (buffer) { buffer['parenthesisLevel']++; }, 'parenthesisLevel--': function (buffer) { buffer['parenthesisLevel']--; }, 'state of aggregation': function (buffer, m) { return { type_: 'state of aggregation', p1: mhchemParser.go(m, 'o') }; }, 'comma': function (buffer, m) { var a = m.replace(/\s*$/, ''); var withSpace = (a !== m); if (withSpace && buffer['parenthesisLevel'] === 0) { return { type_: 'comma enumeration L', p1: a }; } else { return { type_: 'comma enumeration M', p1: a }; } }, 'output': function (buffer, m, entityFollows) { // entityFollows: // undefined = if we have nothing else to output, also ignore the just read space (buffer.sb) // 1 = an entity follows, never omit the space if there was one just read before (can only apply to state 1) // 2 = 1 + the entity can have an amount, so output a\, instead of converting it to o (can only apply to states a|as) /** @type {ParserOutput | ParserOutput[]} */ var ret; if (!buffer.r) { ret = []; if (!buffer.a && !buffer.b && !buffer.p && !buffer.o && !buffer.q && !buffer.d && !entityFollows) { //ret = []; } else { if (buffer.sb) { ret.push({ type_: 'entitySkip' }); } if (!buffer.o && !buffer.q && !buffer.d && !buffer.b && !buffer.p && entityFollows!==2) { buffer.o = buffer.a; buffer.a = undefined; } else if (!buffer.o && !buffer.q && !buffer.d && (buffer.b || buffer.p)) { buffer.o = buffer.a; buffer.d = buffer.b; buffer.q = buffer.p; buffer.a = buffer.b = buffer.p = undefined; } else { if (buffer.o && buffer.dType==='kv' && mhchemParser.patterns.match_('d-oxidation$', buffer.d || "")) { buffer.dType = 'oxidation'; } else if (buffer.o && buffer.dType==='kv' && !buffer.q) { buffer.dType = undefined; } } ret.push({ type_: 'chemfive', a: mhchemParser.go(buffer.a, 'a'), b: mhchemParser.go(buffer.b, 'bd'), p: mhchemParser.go(buffer.p, 'pq'), o: mhchemParser.go(buffer.o, 'o'), q: mhchemParser.go(buffer.q, 'pq'), d: mhchemParser.go(buffer.d, (buffer.dType === 'oxidation' ? 'oxidation' : 'bd')), dType: buffer.dType }); } } else { // r /** @type {ParserOutput[]} */ var rd; if (buffer.rdt === 'M') { rd = mhchemParser.go(buffer.rd, 'tex-math'); } else if (buffer.rdt === 'T') { rd = [ { type_: 'text', p1: buffer.rd || "" } ]; } else { rd = mhchemParser.go(buffer.rd); } /** @type {ParserOutput[]} */ var rq; if (buffer.rqt === 'M') { rq = mhchemParser.go(buffer.rq, 'tex-math'); } else if (buffer.rqt === 'T') { rq = [ { type_: 'text', p1: buffer.rq || ""} ]; } else { rq = mhchemParser.go(buffer.rq); } ret = { type_: 'arrow', r: buffer.r, rd: rd, rq: rq }; } for (var p in buffer) { if (p !== 'parenthesisLevel' && p !== 'beginsWithBond') { delete buffer[p]; } } return ret; }, 'oxidation-output': function (buffer, m) { var ret = [ "{" ]; mhchemParser.concatArray(ret, mhchemParser.go(m, 'oxidation')); ret.push("}"); return ret; }, 'frac-output': function (buffer, m) { return { type_: 'frac-ce', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) }; }, 'overset-output': function (buffer, m) { return { type_: 'overset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) }; }, 'underset-output': function (buffer, m) { return { type_: 'underset', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) }; }, 'underbrace-output': function (buffer, m) { return { type_: 'underbrace', p1: mhchemParser.go(m[0]), p2: mhchemParser.go(m[1]) }; }, 'color-output': function (buffer, m) { return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1]) }; }, 'r=': function (buffer, m) { buffer.r = m; }, 'rdt=': function (buffer, m) { buffer.rdt = m; }, 'rd=': function (buffer, m) { buffer.rd = m; }, 'rqt=': function (buffer, m) { buffer.rqt = m; }, 'rq=': function (buffer, m) { buffer.rq = m; }, 'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; } } }, 'a': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, '1/2$': { '0': { action_: '1/2' } }, 'else': { '0': { nextState: '1', revisit: true } }, '$(...)$': { '*': { action_: 'tex-math tight', nextState: '1' } }, ',': { '*': { action_: { type_: 'insert', option: 'commaDecimal' } } }, 'else2': { '*': { action_: 'copy' } } }), actions: {} }, 'o': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, '1/2$': { '0': { action_: '1/2' } }, 'else': { '0': { nextState: '1', revisit: true } }, 'letters': { '*': { action_: 'rm' } }, '\\ca': { '*': { action_: { type_: 'insert', option: 'circa' } } }, '\\x{}{}|\\x{}|\\x': { '*': { action_: 'copy' } }, '${(...)}$|$(...)$': { '*': { action_: 'tex-math' } }, '{(...)}': { '*': { action_: '{text}' } }, 'else2': { '*': { action_: 'copy' } } }), actions: {} }, 'text': { transitions: mhchemParser.createTransitions({ 'empty': { '*': { action_: 'output' } }, '{...}': { '*': { action_: 'text=' } }, '${(...)}$|$(...)$': { '*': { action_: 'tex-math' } }, '\\greek': { '*': { action_: [ 'output', 'rm' ] } }, '\\,|\\x{}{}|\\x{}|\\x': { '*': { action_: [ 'output', 'copy' ] } }, 'else': { '*': { action_: 'text=' } } }), actions: { 'output': function (buffer) { if (buffer.text_) { /** @type {ParserOutput} */ var ret = { type_: 'text', p1: buffer.text_ }; for (var p in buffer) { delete buffer[p]; } return ret; } } } }, 'pq': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, 'state of aggregation $': { '*': { action_: 'state of aggregation' } }, 'i$': { '0': { nextState: '!f', revisit: true } }, '(KV letters),': { '0': { action_: 'rm', nextState: '0' } }, 'formula$': { '0': { nextState: 'f', revisit: true } }, '1/2$': { '0': { action_: '1/2' } }, 'else': { '0': { nextState: '!f', revisit: true } }, '${(...)}$|$(...)$': { '*': { action_: 'tex-math' } }, '{(...)}': { '*': { action_: 'text' } }, 'a-z': { 'f': { action_: 'tex-math' } }, 'letters': { '*': { action_: 'rm' } }, '-9.,9': { '*': { action_: '9,9' } }, ',': { '*': { action_: { type_: 'insert+p1', option: 'comma enumeration S' } } }, '\\color{(...)}{(...)}1|\\color(...){(...)}2': { '*': { action_: 'color-output' } }, '\\color{(...)}0': { '*': { action_: 'color0-output' } }, '\\ce{(...)}': { '*': { action_: 'ce' } }, '\\,|\\x{}{}|\\x{}|\\x': { '*': { action_: 'copy' } }, 'else2': { '*': { action_: 'copy' } } }), actions: { 'state of aggregation': function (buffer, m) { return { type_: 'state of aggregation subscript', p1: mhchemParser.go(m, 'o') }; }, 'color-output': function (buffer, m) { return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'pq') }; } } }, 'bd': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, 'x$': { '0': { nextState: '!f', revisit: true } }, 'formula$': { '0': { nextState: 'f', revisit: true } }, 'else': { '0': { nextState: '!f', revisit: true } }, '-9.,9 no missing 0': { '*': { action_: '9,9' } }, '.': { '*': { action_: { type_: 'insert', option: 'electron dot' } } }, 'a-z': { 'f': { action_: 'tex-math' } }, 'x': { '*': { action_: { type_: 'insert', option: 'KV x' } } }, 'letters': { '*': { action_: 'rm' } }, '\'': { '*': { action_: { type_: 'insert', option: 'prime' } } }, '${(...)}$|$(...)$': { '*': { action_: 'tex-math' } }, '{(...)}': { '*': { action_: 'text' } }, '\\color{(...)}{(...)}1|\\color(...){(...)}2': { '*': { action_: 'color-output' } }, '\\color{(...)}0': { '*': { action_: 'color0-output' } }, '\\ce{(...)}': { '*': { action_: 'ce' } }, '\\,|\\x{}{}|\\x{}|\\x': { '*': { action_: 'copy' } }, 'else2': { '*': { action_: 'copy' } } }), actions: { 'color-output': function (buffer, m) { return { type_: 'color', color1: m[0], color2: mhchemParser.go(m[1], 'bd') }; } } }, 'oxidation': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, 'roman numeral': { '*': { action_: 'roman-numeral' } }, '${(...)}$|$(...)$': { '*': { action_: 'tex-math' } }, 'else': { '*': { action_: 'copy' } } }), actions: { 'roman-numeral': function (buffer, m) { return { type_: 'roman numeral', p1: m || "" }; } } }, 'tex-math': { transitions: mhchemParser.createTransitions({ 'empty': { '*': { action_: 'output' } }, '\\ce{(...)}': { '*': { action_: [ 'output', 'ce' ] } }, '{...}|\\,|\\x{}{}|\\x{}|\\x': { '*': { action_: 'o=' } }, 'else': { '*': { action_: 'o=' } } }), actions: { 'output': function (buffer) { if (buffer.o) { /** @type {ParserOutput} */ var ret = { type_: 'tex-math', p1: buffer.o }; for (var p in buffer) { delete buffer[p]; } return ret; } } } }, 'tex-math tight': { transitions: mhchemParser.createTransitions({ 'empty': { '*': { action_: 'output' } }, '\\ce{(...)}': { '*': { action_: [ 'output', 'ce' ] } }, '{...}|\\,|\\x{}{}|\\x{}|\\x': { '*': { action_: 'o=' } }, '-|+': { '*': { action_: 'tight operator' } }, 'else': { '*': { action_: 'o=' } } }), actions: { 'tight operator': function (buffer, m) { buffer.o = (buffer.o || "") + "{"+m+"}"; }, 'output': function (buffer) { if (buffer.o) { /** @type {ParserOutput} */ var ret = { type_: 'tex-math', p1: buffer.o }; for (var p in buffer) { delete buffer[p]; } return ret; } } } }, '9,9': { transitions: mhchemParser.createTransitions({ 'empty': { '*': {} }, ',': { '*': { action_: 'comma' } }, 'else': { '*': { action_: 'copy' } } }), actions: { 'comma': function () { return { type_: 'commaDecimal' }; } } }, //#endregion // // \pu state machines // //#region pu 'pu': { transitions: mhchemParser.createTransitions({ 'empty': { '*': { action_: 'output' } }, 'space$': { '*': { action_: [ 'output', 'space' ] } }, '{[(|)]}': { '0|a': { action_: 'copy' } }, '(-)(9)^(-9)': { '0': { action_: 'number^', nextState: 'a' } }, '(-)(9.,9)(e)(99)': { '0': { action_: 'enumber', nextState: 'a' } }, 'space': { '0|a': {} }, 'pm-operator': { '0|a': { action_: { type_: 'operator', option: '\\pm' }, nextState: '0' } }, 'operator': { '0|a': { action_: 'copy', nextState: '0' } }, '//': { 'd': { action_: 'o=', nextState: '/' } }, '/': { 'd': { action_: 'o=', nextState: '/' } }, '{...}|else': { '0|d': { action_: 'd=', nextState: 'd' }, 'a': { action_: [ 'space', 'd=' ], nextState: 'd' }, '/|q': { action_: 'q=', nextState: 'q' } } }), actions: { 'enumber': function (buffer, m) { /** @type {ParserOutput[]} */ var ret = []; if (m[0] === "+-" || m[0] === "+/-") { ret.push("\\pm "); } else if (m[0]) { ret.push(m[0]); } if (m[1]) { mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); if (m[2]) { if (m[2].match(/[,.]/)) { mhchemParser.concatArray(ret, mhchemParser.go(m[2], 'pu-9,9')); } else { ret.push(m[2]); } } m[3] = m[4] || m[3]; if (m[3]) { m[3] = m[3].trim(); if (m[3] === "e" || m[3].substr(0, 1) === "*") { ret.push({ type_: 'cdot' }); } else { ret.push({ type_: 'times' }); } } } if (m[3]) { ret.push("10^{"+m[5]+"}"); } return ret; }, 'number^': function (buffer, m) { /** @type {ParserOutput[]} */ var ret = []; if (m[0] === "+-" || m[0] === "+/-") { ret.push("\\pm "); } else if (m[0]) { ret.push(m[0]); } mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); ret.push("^{"+m[2]+"}"); return ret; }, 'operator': function (buffer, m, p1) { return { type_: 'operator', kind_: (p1 || m) }; }, 'space': function () { return { type_: 'pu-space-1' }; }, 'output': function (buffer) { /** @type {ParserOutput | ParserOutput[]} */ var ret; var md = mhchemParser.patterns.match_('{(...)}', buffer.d || ""); if (md && md.remainder === '') { buffer.d = md.match_; } var mq = mhchemParser.patterns.match_('{(...)}', buffer.q || ""); if (mq && mq.remainder === '') { buffer.q = mq.match_; } if (buffer.d) { buffer.d = buffer.d.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); buffer.d = buffer.d.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); } if (buffer.q) { // fraction buffer.q = buffer.q.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); buffer.q = buffer.q.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); var b5 = { d: mhchemParser.go(buffer.d, 'pu'), q: mhchemParser.go(buffer.q, 'pu') }; if (buffer.o === '//') { ret = { type_: 'pu-frac', p1: b5.d, p2: b5.q }; } else { ret = b5.d; if (b5.d.length > 1 || b5.q.length > 1) { ret.push({ type_: ' / ' }); } else { ret.push({ type_: '/' }); } mhchemParser.concatArray(ret, b5.q); } } else { // no fraction ret = mhchemParser.go(buffer.d, 'pu-2'); } for (var p in buffer) { delete buffer[p]; } return ret; } } }, 'pu-2': { transitions: mhchemParser.createTransitions({ 'empty': { '*': { action_: 'output' } }, '*': { '*': { action_: [ 'output', 'cdot' ], nextState: '0' } }, '\\x': { '*': { action_: 'rm=' } }, 'space': { '*': { action_: [ 'output', 'space' ], nextState: '0' } }, '^{(...)}|^(-1)': { '1': { action_: '^(-1)' } }, '-9.,9': { '0': { action_: 'rm=', nextState: '0' }, '1': { action_: '^(-1)', nextState: '0' } }, '{...}|else': { '*': { action_: 'rm=', nextState: '1' } } }), actions: { 'cdot': function () { return { type_: 'tight cdot' }; }, '^(-1)': function (buffer, m) { buffer.rm += "^{"+m+"}"; }, 'space': function () { return { type_: 'pu-space-2' }; }, 'output': function (buffer) { /** @type {ParserOutput | ParserOutput[]} */ var ret = []; if (buffer.rm) { var mrm = mhchemParser.patterns.match_('{(...)}', buffer.rm || ""); if (mrm && mrm.remainder === '') { ret = mhchemParser.go(mrm.match_, 'pu'); } else { ret = { type_: 'rm', p1: buffer.rm }; } } for (var p in buffer) { delete buffer[p]; } return ret; } } }, 'pu-9,9': { transitions: mhchemParser.createTransitions({ 'empty': { '0': { action_: 'output-0' }, 'o': { action_: 'output-o' } }, ',': { '0': { action_: [ 'output-0', 'comma' ], nextState: 'o' } }, '.': { '0': { action_: [ 'output-0', 'copy' ], nextState: 'o' } }, 'else': { '*': { action_: 'text=' } } }), actions: { 'comma': function () { return { type_: 'commaDecimal' }; }, 'output-0': function (buffer) { /** @type {ParserOutput[]} */ var ret = []; buffer.text_ = buffer.text_ || ""; if (buffer.text_.length > 4) { var a = buffer.text_.length % 3; if (a === 0) { a = 3; } for (var i=buffer.text_.length-3; i>0; i-=3) { ret.push(buffer.text_.substr(i, 3)); ret.push({ type_: '1000 separator' }); } ret.push(buffer.text_.substr(0, a)); ret.reverse(); } else { ret.push(buffer.text_); } for (var p in buffer) { delete buffer[p]; } return ret; }, 'output-o': function (buffer) { /** @type {ParserOutput[]} */ var ret = []; buffer.text_ = buffer.text_ || ""; if (buffer.text_.length > 4) { var a = buffer.text_.length - 3; for (var i=0; i": return "rightarrow"; case "\u2192": return "rightarrow"; case "\u27F6": return "rightarrow"; case "<-": return "leftarrow"; case "<->": return "leftrightarrow"; case "<-->": return "rightleftarrows"; case "<=>": return "rightleftharpoons"; case "\u21CC": return "rightleftharpoons"; case "<=>>": return "rightequilibrium"; case "<<=>": return "leftequilibrium"; default: assertNever(a); throw ["MhchemBugT", "mhchem bug T. Please report."]; } }, _getBond: function (a) { switch (a) { case "-": return "{-}"; case "1": return "{-}"; case "=": return "{=}"; case "2": return "{=}"; case "#": return "{\\equiv}"; case "3": return "{\\equiv}"; case "~": return "{\\tripledash}"; case "~-": return "{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}"; case "~=": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; case "~--": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; case "-~-": return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}"; case "...": return "{{\\cdot}{\\cdot}{\\cdot}}"; case "....": return "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}"; case "->": return "{\\rightarrow}"; case "<-": return "{\\leftarrow}"; case "<": return "{<}"; case ">": return "{>}"; default: assertNever(a); throw ["MhchemBugT", "mhchem bug T. Please report."]; } }, _getOperator: function (a) { switch (a) { case "+": return " {}+{} "; case "-": return " {}-{} "; case "=": return " {}={} "; case "<": return " {}<{} "; case ">": return " {}>{} "; case "<<": return " {}\\ll{} "; case ">>": return " {}\\gg{} "; case "\\pm": return " {}\\pm{} "; case "\\approx": return " {}\\approx{} "; case "$\\approx$": return " {}\\approx{} "; case "v": return " \\downarrow{} "; case "(v)": return " \\downarrow{} "; case "^": return " \\uparrow{} "; case "(^)": return " \\uparrow{} "; default: assertNever(a); throw ["MhchemBugT", "mhchem bug T. Please report."]; } } }; // // Helpers for code anaylsis // Will show type error at calling position // /** @param {number} a */ function assertNever(a) {} /** @param {string} a */ function assertString(a) {} ================================================ FILE: server/modules/rendering/markdown-katex/renderer.js ================================================ const katex = require('katex') const chemParse = require('./mhchem') /* global WIKI */ // ------------------------------------ // Markdown - KaTeX Renderer // ------------------------------------ // // Includes code from https://github.com/liradb2000/markdown-it-katex // Add \ce, \pu, and \tripledash to the KaTeX macros. katex.__defineMacro('\\ce', function(context) { return chemParse(context.consumeArgs(1)[0], 'ce') }) katex.__defineMacro('\\pu', function(context) { return chemParse(context.consumeArgs(1)[0], 'pu') }) // Needed for \bond for the ~ forms // Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not // a mathematical minus, U+2212. So we need that extra 0.56. katex.__defineMacro('\\tripledash', '{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu' + '\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}') module.exports = { init (mdinst, conf) { const macros = {} if (conf.useInline) { mdinst.inline.ruler.after('escape', 'katex_inline', katexInline) mdinst.renderer.rules.katex_inline = (tokens, idx) => { try { return katex.renderToString(tokens[idx].content, { displayMode: false, macros }) } catch (err) { WIKI.logger.warn(err) return tokens[idx].content } } } if (conf.useBlocks) { mdinst.block.ruler.after('blockquote', 'katex_block', katexBlock, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) mdinst.renderer.rules.katex_block = (tokens, idx) => { try { return `

    ` + katex.renderToString(tokens[idx].content, { displayMode: true, macros }) + `

    ` } catch (err) { WIKI.logger.warn(err) return tokens[idx].content } } } } } // Test if potential opening or closing delimieter // Assumes that there is a "$" at state.src[pos] function isValidDelim (state, pos) { let prevChar let nextChar let max = state.posMax let canOpen = true let canClose = true prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1 nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1 // Check non-whitespace conditions for opening and closing, and // check that closing delimeter isn't followed by a number if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { canClose = false } if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { canOpen = false } return { canOpen: canOpen, canClose: canClose } } function katexInline (state, silent) { let start, match, token, res, pos if (state.src[state.pos] !== '$') { return false } res = isValidDelim(state, state.pos) if (!res.canOpen) { if (!silent) { state.pending += '$' } state.pos += 1 return true } // First check for and bypass all properly escaped delimieters // This loop will assume that the first leading backtick can not // be the first character in state.src, which is known since // we have found an opening delimieter already. start = state.pos + 1 match = start while ((match = state.src.indexOf('$', match)) !== -1) { // Found potential $, look for escapes, pos will point to // first non escape when complete pos = match - 1 while (state.src[pos] === '\\') { pos -= 1 } // Even number of escapes, potential closing delimiter found if (((match - pos) % 2) === 1) { break } match += 1 } // No closing delimter found. Consume $ and continue. if (match === -1) { if (!silent) { state.pending += '$' } state.pos = start return true } // Check if we have empty content, ie: $$. Do not parse. if (match - start === 0) { if (!silent) { state.pending += '$$' } state.pos = start + 1 return true } // Check for valid closing delimiter res = isValidDelim(state, match) if (!res.canClose) { if (!silent) { state.pending += '$' } state.pos = start return true } if (!silent) { token = state.push('katex_inline', 'math', 0) token.markup = '$' token.content = state.src.slice(start, match) } state.pos = match + 1 return true } function katexBlock (state, start, end, silent) { let firstLine; let lastLine; let next; let lastPos; let found = false; let token let pos = state.bMarks[start] + state.tShift[start] let max = state.eMarks[start] if (pos + 2 > max) { return false } if (state.src.slice(pos, pos + 2) !== '$$') { return false } pos += 2 firstLine = state.src.slice(pos, max) if (silent) { return true } if (firstLine.trim().slice(-2) === '$$') { // Single line expression firstLine = firstLine.trim().slice(0, -2) found = true } for (next = start; !found;) { next++ if (next >= end) { break } pos = state.bMarks[next] + state.tShift[next] max = state.eMarks[next] if (pos < max && state.tShift[next] < state.blkIndent) { // non-empty line with negative indent should stop the list: break } if (state.src.slice(pos, max).trim().slice(-2) === '$$') { lastPos = state.src.slice(0, max).lastIndexOf('$$') lastLine = state.src.slice(pos, lastPos) found = true } } state.line = next + 1 token = state.push('katex_block', 'math', 0) token.block = true token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : '') token.map = [ start, state.line ] token.markup = '$$' return true } ================================================ FILE: server/modules/rendering/markdown-kroki/definition.yml ================================================ key: markdownKroki title: Kroki description: Kroki Diagrams Parser author: rlanyi (based on PlantUML renderer) icon: mdi-sitemap enabledDefault: false dependsOn: markdownCore props: server: type: String default: https://kroki.io title: Kroki Server hint: Kroki server used for image generation order: 1 public: true openMarker: type: String default: "```kroki" title: Open Marker hint: String to use as opening delimiter. Diagram type must be put in the next line in lowercase. order: 2 public: true closeMarker: type: String default: "```" title: Close Marker hint: String to use as closing delimiter order: 3 public: true ================================================ FILE: server/modules/rendering/markdown-kroki/renderer.js ================================================ const zlib = require('zlib') // ------------------------------------ // Markdown - Kroki Preprocessor // ------------------------------------ module.exports = { init (mdinst, conf) { mdinst.use((md, opts) => { const openMarker = opts.openMarker || '```kroki' const openChar = openMarker.charCodeAt(0) const closeMarker = opts.closeMarker || '```' const closeChar = closeMarker.charCodeAt(0) const server = opts.server || 'https://kroki.io' md.block.ruler.before('fence', 'kroki', (state, startLine, endLine, silent) => { let nextLine let markup let params let token let i let autoClosed = false let start = state.bMarks[startLine] + state.tShift[startLine] let max = state.eMarks[startLine] // Check out the first character quickly, // this should filter out most of non-uml blocks // if (openChar !== state.src.charCodeAt(start)) { return false } // Check out the rest of the marker string // for (i = 0; i < openMarker.length; ++i) { if (openMarker[i] !== state.src[start + i]) { return false } } markup = state.src.slice(start, start + i) params = state.src.slice(start + i, max) // Since start is found, we can report success here in validation mode // if (silent) { return true } // Search for the end of the block // nextLine = startLine for (;;) { nextLine++ if (nextLine >= endLine) { // unclosed block should be autoclosed by end of document. // also block seems to be autoclosed by end of parent break } start = state.bMarks[nextLine] + state.tShift[nextLine] max = state.eMarks[nextLine] if (start < max && state.sCount[nextLine] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test break } if (closeChar !== state.src.charCodeAt(start)) { // didn't find the closing fence continue } if (state.sCount[nextLine] > state.sCount[startLine]) { // closing fence should not be indented with respect of opening fence continue } let closeMarkerMatched = true for (i = 0; i < closeMarker.length; ++i) { if (closeMarker[i] !== state.src[start + i]) { closeMarkerMatched = false break } } if (!closeMarkerMatched) { continue } // make sure tail has spaces only if (state.skipSpaces(start + i) < max) { continue } // found! autoClosed = true break } let contents = state.src .split('\n') .slice(startLine + 1, nextLine) .join('\n') // We generate a token list for the alt property, to mimic what the image parser does. let altToken = [] // Remove leading space if any. let alt = params ? params.slice(1) : 'uml diagram' state.md.inline.parse( alt, state.md, state.env, altToken ) let firstlf = contents.indexOf('\n') if (firstlf === -1) firstlf = undefined let diagramType = contents.substring(0, firstlf) contents = contents.substring(firstlf + 1) let result = zlib.deflateSync(contents).toString('base64').replace(/\+/g, '-').replace(/\//g, '_') token = state.push('kroki', 'img', 0) // alt is constructed from children. No point in populating it here. token.attrs = [ [ 'src', `${server}/${diagramType}/svg/${result}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ] token.block = true token.children = altToken token.info = params token.map = [ startLine, nextLine ] token.markup = markup state.line = nextLine + (autoClosed ? 1 : 0) return true }, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) md.renderer.rules.kroki = md.renderer.rules.image }, { openMarker: conf.openMarker, closeMarker: conf.closeMarker, server: conf.server }) } } ================================================ FILE: server/modules/rendering/markdown-mathjax/definition.yml ================================================ key: markdownMathjax title: Mathjax description: LaTeX Math + Chemical Expression Typesetting Renderer author: requarks.io icon: mdi-math-integral enabledDefault: false dependsOn: markdownCore props: useInline: type: Boolean default: true title: Inline TeX hint: Process inline TeX expressions surrounded by $ symbols. order: 1 useBlocks: type: Boolean default: true title: TeX Blocks hint: Process TeX blocks enclosed by $$ symbols. order: 2 ================================================ FILE: server/modules/rendering/markdown-mathjax/renderer.js ================================================ const mjax = require('mathjax') /* global WIKI */ // ------------------------------------ // Markdown - MathJax Renderer // ------------------------------------ const extensions = [ 'bbox', 'boldsymbol', 'braket', 'color', 'extpfeil', 'mhchem', 'newcommand', 'unicode', 'verb' ] module.exports = { async init (mdinst, conf) { const MathJax = await mjax.init({ loader: { require: require, paths: { mathjax: 'mathjax/es5' }, load: [ 'input/tex', 'output/svg', ...extensions.map(e => `[tex]/${e}`) ] }, tex: { packages: {'[+]': extensions} } }) if (conf.useInline) { mdinst.inline.ruler.after('escape', 'mathjax_inline', mathjaxInline) mdinst.renderer.rules.mathjax_inline = (tokens, idx) => { try { const result = MathJax.tex2svg(tokens[idx].content, { display: false }) return MathJax.startup.adaptor.innerHTML(result) } catch (err) { WIKI.logger.warn(err) return tokens[idx].content } } } if (conf.useBlocks) { mdinst.block.ruler.after('blockquote', 'mathjax_block', mathjaxBlock, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) mdinst.renderer.rules.mathjax_block = (tokens, idx) => { try { const result = MathJax.tex2svg(tokens[idx].content, { display: true }) return `

    ` + MathJax.startup.adaptor.innerHTML(result) + `

    ` } catch (err) { WIKI.logger.warn(err) return tokens[idx].content } } } } } // Test if potential opening or closing delimieter // Assumes that there is a "$" at state.src[pos] function isValidDelim (state, pos) { let prevChar let nextChar let max = state.posMax let canOpen = true let canClose = true prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1 nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1 // Check non-whitespace conditions for opening and closing, and // check that closing delimeter isn't followed by a number if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { canClose = false } if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { canOpen = false } return { canOpen: canOpen, canClose: canClose } } function mathjaxInline (state, silent) { let start, match, token, res, pos if (state.src[state.pos] !== '$') { return false } res = isValidDelim(state, state.pos) if (!res.canOpen) { if (!silent) { state.pending += '$' } state.pos += 1 return true } // First check for and bypass all properly escaped delimieters // This loop will assume that the first leading backtick can not // be the first character in state.src, which is known since // we have found an opening delimieter already. start = state.pos + 1 match = start while ((match = state.src.indexOf('$', match)) !== -1) { // Found potential $, look for escapes, pos will point to // first non escape when complete pos = match - 1 while (state.src[pos] === '\\') { pos -= 1 } // Even number of escapes, potential closing delimiter found if (((match - pos) % 2) === 1) { break } match += 1 } // No closing delimter found. Consume $ and continue. if (match === -1) { if (!silent) { state.pending += '$' } state.pos = start return true } // Check if we have empty content, ie: $$. Do not parse. if (match - start === 0) { if (!silent) { state.pending += '$$' } state.pos = start + 1 return true } // Check for valid closing delimiter res = isValidDelim(state, match) if (!res.canClose) { if (!silent) { state.pending += '$' } state.pos = start return true } if (!silent) { token = state.push('mathjax_inline', 'math', 0) token.markup = '$' token.content = state.src.slice(start, match) } state.pos = match + 1 return true } function mathjaxBlock (state, start, end, silent) { let firstLine; let lastLine; let next; let lastPos; let found = false; let token let pos = state.bMarks[start] + state.tShift[start] let max = state.eMarks[start] if (pos + 2 > max) { return false } if (state.src.slice(pos, pos + 2) !== '$$') { return false } pos += 2 firstLine = state.src.slice(pos, max) if (silent) { return true } if (firstLine.trim().slice(-2) === '$$') { // Single line expression firstLine = firstLine.trim().slice(0, -2) found = true } for (next = start; !found;) { next++ if (next >= end) { break } pos = state.bMarks[next] + state.tShift[next] max = state.eMarks[next] if (pos < max && state.tShift[next] < state.blkIndent) { // non-empty line with negative indent should stop the list: break } if (state.src.slice(pos, max).trim().slice(-2) === '$$') { lastPos = state.src.slice(0, max).lastIndexOf('$$') lastLine = state.src.slice(pos, lastPos) found = true } } state.line = next + 1 token = state.push('mathjax_block', 'math', 0) token.block = true token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : '') token.map = [ start, state.line ] token.markup = '$$' return true } ================================================ FILE: server/modules/rendering/markdown-multi-table/definition.yml ================================================ key: markdownMultiTable title: MultiMarkdown Table description: Add MultiMarkdown table support author: requarks.io icon: mdi-table enabledDefault: false dependsOn: markdownCore props: multilineEnabled: type: Boolean title: Multiline hint: Enable multiple lines rows default: true headerlessEnabled: type: Boolean title: Headerless hint: Enable ommited table headers default: true rowspanEnabled: type: Boolean title: Rowspan hint: Enable table row spans default: true ================================================ FILE: server/modules/rendering/markdown-multi-table/renderer.js ================================================ const multiTable = require('markdown-it-multimd-table') module.exports = { init (md, conf) { md.use(multiTable, { multiline: conf.multilineEnabled, rowspan: conf.rowspanEnabled, headerless: conf.headerlessEnabled }) } } ================================================ FILE: server/modules/rendering/markdown-pivot-table/definition.yml ================================================ key: markdownPivotTable title: Pivot Table description: Add pivot table support author: jaeseopark icon: mdi-table enabledDefault: false dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-pivot-table/renderer.js ================================================ const pivotTable = require('markdown-it-pivot-table') module.exports = { init (md) { md.use(pivotTable) } } ================================================ FILE: server/modules/rendering/markdown-plantuml/definition.yml ================================================ key: markdownPlantuml title: PlantUML description: PlantUML Markdown Parser author: ethanmdavidson icon: mdi-sitemap enabledDefault: true dependsOn: markdownCore props: server: type: String default: https://plantuml.requarks.io title: PlantUML Server hint: PlantUML server used for image generation order: 1 public: true openMarker: type: String default: "```plantuml" title: Open Marker hint: String to use as opening delimiter order: 2 public: true closeMarker: type: String default: "```" title: Close Marker hint: String to use as closing delimiter order: 3 public: true imageFormat: type: String default: svg title: Image Format hint: Format to use for rendered PlantUML images enum: - svg - png - latex - ascii order: 4 public: true ================================================ FILE: server/modules/rendering/markdown-plantuml/renderer.js ================================================ const zlib = require('zlib') // ------------------------------------ // Markdown - PlantUML Preprocessor // ------------------------------------ module.exports = { init (mdinst, conf) { mdinst.use((md, opts) => { const openMarker = opts.openMarker || '```plantuml' const openChar = openMarker.charCodeAt(0) const closeMarker = opts.closeMarker || '```' const closeChar = closeMarker.charCodeAt(0) const imageFormat = opts.imageFormat || 'svg' const server = opts.server || 'https://plantuml.requarks.io' md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => { let nextLine let markup let params let token let i let autoClosed = false let start = state.bMarks[startLine] + state.tShift[startLine] let max = state.eMarks[startLine] // Check out the first character quickly, // this should filter out most of non-uml blocks // if (openChar !== state.src.charCodeAt(start)) { return false } // Check out the rest of the marker string // for (i = 0; i < openMarker.length; ++i) { if (openMarker[i] !== state.src[start + i]) { return false } } markup = state.src.slice(start, start + i) params = state.src.slice(start + i, max) // Since start is found, we can report success here in validation mode // if (silent) { return true } // Search for the end of the block // nextLine = startLine for (;;) { nextLine++ if (nextLine >= endLine) { // unclosed block should be autoclosed by end of document. // also block seems to be autoclosed by end of parent break } start = state.bMarks[nextLine] + state.tShift[nextLine] max = state.eMarks[nextLine] if (start < max && state.sCount[nextLine] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test break } if (closeChar !== state.src.charCodeAt(start)) { // didn't find the closing fence continue } if (state.sCount[nextLine] > state.sCount[startLine]) { // closing fence should not be indented with respect of opening fence continue } let closeMarkerMatched = true for (i = 0; i < closeMarker.length; ++i) { if (closeMarker[i] !== state.src[start + i]) { closeMarkerMatched = false break } } if (!closeMarkerMatched) { continue } // make sure tail has spaces only if (state.skipSpaces(start + i) < max) { continue } // found! autoClosed = true break } const contents = state.src .split('\n') .slice(startLine + 1, nextLine) .join('\n') // We generate a token list for the alt property, to mimic what the image parser does. let altToken = [] // Remove leading space if any. let alt = params ? params.slice(1) : 'uml diagram' state.md.inline.parse( alt, state.md, state.env, altToken ) const zippedCode = encode64(zlib.deflateRawSync('@startuml\n' + contents + '\n@enduml').toString('binary')) token = state.push('uml_diagram', 'img', 0) // alt is constructed from children. No point in populating it here. token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ] token.block = true token.children = altToken token.info = params token.map = [ startLine, nextLine ] token.markup = markup state.line = nextLine + (autoClosed ? 1 : 0) return true }, { alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] }) md.renderer.rules.uml_diagram = md.renderer.rules.image }, { openMarker: conf.openMarker, closeMarker: conf.closeMarker, imageFormat: conf.imageFormat, server: conf.server }) } } function encode64 (data) { let r = '' for (let i = 0; i < data.length; i += 3) { if (i + 2 === data.length) { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0) } else if (i + 1 === data.length) { r += append3bytes(data.charCodeAt(i), 0, 0) } else { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2)) } } return r } function append3bytes (b1, b2, b3) { let c1 = b1 >> 2 let c2 = ((b1 & 0x3) << 4) | (b2 >> 4) let c3 = ((b2 & 0xF) << 2) | (b3 >> 6) let c4 = b3 & 0x3F let r = '' r += encode6bit(c1 & 0x3F) r += encode6bit(c2 & 0x3F) r += encode6bit(c3 & 0x3F) r += encode6bit(c4 & 0x3F) return r } function encode6bit(raw) { let b = raw if (b < 10) { return String.fromCharCode(48 + b) } b -= 10 if (b < 26) { return String.fromCharCode(65 + b) } b -= 26 if (b < 26) { return String.fromCharCode(97 + b) } b -= 26 if (b === 0) { return '-' } if (b === 1) { return '_' } return '?' } ================================================ FILE: server/modules/rendering/markdown-supsub/definition.yml ================================================ key: markdownSupsub title: Subscript/Superscript description: Parse subscript and superscript tags author: requarks.io icon: mdi-format-superscript enabledDefault: true dependsOn: markdownCore props: subEnabled: type: Boolean title: Subscript hint: Enable subscript tags default: true supEnabled: type: Boolean title: Superscript hint: Enable superscript tags default: true ================================================ FILE: server/modules/rendering/markdown-supsub/renderer.js ================================================ const mdSub = require('markdown-it-sub') const mdSup = require('markdown-it-sup') // ------------------------------------ // Markdown - Subscript / Superscript // ------------------------------------ module.exports = { init (md, conf) { if (conf.subEnabled) { md.use(mdSub) } if (conf.supEnabled) { md.use(mdSup) } } } ================================================ FILE: server/modules/rendering/markdown-tasklists/definition.yml ================================================ key: markdownTasklists title: Task Lists description: Parse task lists to checkboxes author: requarks.io icon: mdi-format-list-checks enabledDefault: true dependsOn: markdownCore props: {} ================================================ FILE: server/modules/rendering/markdown-tasklists/renderer.js ================================================ const mdTaskLists = require('markdown-it-task-lists') // ------------------------------------ // Markdown - Task Lists // ------------------------------------ module.exports = { init (md, conf) { md.use(mdTaskLists, { label: false, labelAfter: false }) } } ================================================ FILE: server/modules/rendering/openapi-core/definition.yml ================================================ key: openapiCore title: Core description: Basic OpenAPI Parser author: requarks.io input: openapi output: html icon: mdi-api props: {} ================================================ FILE: server/modules/rendering/openapi-core/renderer.js ================================================ const _ = require('lodash') module.exports = { async render() { let output = this.input for (let child of this.children) { const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) output = await renderer.init(output, child.config) } return output } } ================================================ FILE: server/modules/search/algolia/definition.yml ================================================ key: algolia title: Algolia description: Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations. author: requarks.io logo: https://static.requarks.io/logo/algolia.svg website: https://www.algolia.com/ isAvailable: true props: appId: type: String title: App ID hint: Your Algolia Application ID, found under API Keys order: 1 apiKey: type: String title: Admin API Key hint: Your Algolia Admin API Key, found under API Keys. order: 2 indexName: type: String title: Index Name hint: The name of the index you created under Indices. default: wiki order: 3 ================================================ FILE: server/modules/search/algolia/engine.js ================================================ const _ = require('lodash') const algoliasearch = require('algoliasearch') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') /* global WIKI */ module.exports = { async activate() { // not used }, async deactivate() { // not used }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/ALGOLIA) Initializing...`) this.client = algoliasearch(this.config.appId, this.config.apiKey) this.index = this.client.initIndex(this.config.indexName) // -> Create Search Index WIKI.logger.info(`(SEARCH/ALGOLIA) Setting index configuration...`) await this.index.setSettings({ searchableAttributes: [ 'title', 'description', 'content' ], attributesToRetrieve: [ 'locale', 'path', 'title', 'description' ], advancedSyntax: true }) WIKI.logger.info(`(SEARCH/ALGOLIA) Initialization completed.`) }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { const results = await this.index.search(q, { hitsPerPage: 50 }) return { results: _.map(results.hits, r => ({ id: r.objectID, locale: r.locale, path: r.path, title: r.title, description: r.description })), suggestions: [], totalHits: results.nbHits } } catch (err) { WIKI.logger.warn('Search Engine Error:') WIKI.logger.warn(err) } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await this.index.saveObject({ objectID: page.hash, locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent }) }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await this.index.partialUpdateObject({ objectID: page.hash, title: page.title, description: page.description, content: page.safeContent }) }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await this.index.deleteObject(page.hash) }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await this.index.deleteObject(page.hash) await this.index.saveObject({ objectID: page.destinationHash, locale: page.destinationLocaleCode, path: page.destinationPath, title: page.title, description: page.description, content: page.safeContent }) }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/ALGOLIA) Rebuilding Index...`) await this.index.clearObjects() const MAX_DOCUMENT_BYTES = 10 * Math.pow(2, 10) // 10 KB const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB const MAX_INDEXING_COUNT = 1000 const COMMA_BYTES = Buffer.from(',').byteLength let chunks = [] let bytes = 0 const processDocument = async (cb, doc) => { try { if (doc) { const docBytes = Buffer.from(JSON.stringify(doc)).byteLength // -> Document too large if (docBytes >= MAX_DOCUMENT_BYTES) { throw new Error('Document exceeds maximum size allowed by Algolia.') } // -> Current batch exceeds size hard limit, flush if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) { await flushBuffer() } if (chunks.length > 0) { bytes += COMMA_BYTES } bytes += docBytes chunks.push(doc) // -> Current batch exceeds count soft limit, flush if (chunks.length >= MAX_INDEXING_COUNT) { await flushBuffer() } } else { // -> End of stream, flush await flushBuffer() } cb() } catch (err) { cb(err) } } const flushBuffer = async () => { WIKI.logger.info(`(SEARCH/ALGOLIA) Sending batch of ${chunks.length}...`) try { await this.index.saveObjects( _.map(chunks, doc => ({ objectID: doc.id, locale: doc.locale, path: doc.path, title: doc.title, description: doc.description, content: WIKI.models.pages.cleanHTML(doc.render) })) ) } catch (err) { WIKI.logger.warn('(SEARCH/ALGOLIA) Failed to send batch to Algolia: ', err) } chunks.length = 0 bytes = 0 } await pipeline( WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({ isPublished: true, isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (chunk, enc, cb) => processDocument(cb, chunk), flush: async (cb) => processDocument(cb) }) ) WIKI.logger.info(`(SEARCH/ALGOLIA) Index rebuilt successfully.`) } } ================================================ FILE: server/modules/search/aws/definition.yml ================================================ key: aws title: AWS CloudSearch description: Amazon CloudSearch is a managed service in the AWS Cloud that makes it simple and cost-effective to set up, manage, and scale a search solution for your website or application. author: requarks.io logo: https://static.requarks.io/logo/aws-cloudsearch.svg website: https://aws.amazon.com/cloudsearch/ isAvailable: true props: domain: type: String title: Search Domain hint: The name of your CloudSearch service. order: 1 endpoint: type: String title: Document Endpoint hint: The Document Endpoint specified in the domain AWS console dashboard. order: 2 region: type: String title: Region hint: The AWS datacenter region where the instance was created. default: us-east-1 enum: - ap-northeast-1 - ap-northeast-2 - ap-southeast-1 - ap-southeast-2 - eu-central-1 - eu-west-1 - sa-east-1 - us-east-1 - us-west-1 - us-west-2 order: 3 accessKeyId: type: String title: Access Key ID hint: The Access Key ID with CloudSearchFullAccess role access to the CloudSearch instance. order: 4 secretAccessKey : type: String title: Secret Access Key hint: The Secret Access Key for the Access Key ID provided above. order: 5 AnalysisSchemeLang: type: String title: Analysis Scheme Language hint: The language used to analyse content. default: en enum: - 'ar' - 'bg' - 'ca' - 'cs' - 'da' - 'de' - 'el' - 'en' - 'es' - 'eu' - 'fa' - 'fi' - 'fr' - 'ga' - 'gl' - 'he' - 'hi' - 'hu' - 'hy' - 'id' - 'it' - 'ja' - 'ko' - 'lv' - 'mul' - 'nl' - 'no' - 'pt' - 'ro' - 'ru' - 'sv' - 'th' - 'tr' - 'zh-Hans' - 'zh-Hant' order: 6 ================================================ FILE: server/modules/search/aws/engine.js ================================================ const _ = require('lodash') const AWS = require('aws-sdk') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') /* global WIKI */ module.exports = { async activate() { // not used }, async deactivate() { // not used }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/AWS) Initializing...`) this.client = new AWS.CloudSearch({ apiVersion: '2013-01-01', accessKeyId: this.config.accessKeyId, secretAccessKey: this.config.secretAccessKey, region: this.config.region }) this.clientDomain = new AWS.CloudSearchDomain({ apiVersion: '2013-01-01', endpoint: this.config.endpoint, accessKeyId: this.config.accessKeyId, secretAccessKey: this.config.secretAccessKey, region: this.config.region }) let rebuildIndex = false // -> Define Analysis Schemes const schemes = await this.client.describeAnalysisSchemes({ DomainName: this.config.domain, AnalysisSchemeNames: ['default_anlscheme'] }).promise() if (_.get(schemes, 'AnalysisSchemes', []).length < 1) { WIKI.logger.info(`(SEARCH/AWS) Defining Analysis Scheme...`) await this.client.defineAnalysisScheme({ DomainName: this.config.domain, AnalysisScheme: { AnalysisSchemeLanguage: this.config.AnalysisSchemeLang, AnalysisSchemeName: 'default_anlscheme' } }).promise() rebuildIndex = true } // -> Define Index Fields const fields = await this.client.describeIndexFields({ DomainName: this.config.domain }).promise() if (_.get(fields, 'IndexFields', []).length < 1) { WIKI.logger.info(`(SEARCH/AWS) Defining Index Fields...`) await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'id', IndexFieldType: 'literal' } }).promise() await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'path', IndexFieldType: 'literal' } }).promise() await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'locale', IndexFieldType: 'literal' } }).promise() await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'title', IndexFieldType: 'text', TextOptions: { ReturnEnabled: true, AnalysisScheme: 'default_anlscheme' } } }).promise() await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'description', IndexFieldType: 'text', TextOptions: { ReturnEnabled: true, AnalysisScheme: 'default_anlscheme' } } }).promise() await this.client.defineIndexField({ DomainName: this.config.domain, IndexField: { IndexFieldName: 'content', IndexFieldType: 'text', TextOptions: { ReturnEnabled: false, AnalysisScheme: 'default_anlscheme' } } }).promise() rebuildIndex = true } // -> Define suggester const suggesters = await this.client.describeSuggesters({ DomainName: this.config.domain, SuggesterNames: ['default_suggester'] }).promise() if (_.get(suggesters, 'Suggesters', []).length < 1) { WIKI.logger.info(`(SEARCH/AWS) Defining Suggester...`) await this.client.defineSuggester({ DomainName: this.config.domain, Suggester: { SuggesterName: 'default_suggester', DocumentSuggesterOptions: { SourceField: 'title', FuzzyMatching: 'high' } } }).promise() rebuildIndex = true } // -> Rebuild Index if (rebuildIndex) { WIKI.logger.info(`(SEARCH/AWS) Requesting Index Rebuild...`) await this.client.indexDocuments({ DomainName: this.config.domain }).promise() } WIKI.logger.info(`(SEARCH/AWS) Initialization completed.`) }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { let suggestions = [] const results = await this.clientDomain.search({ query: q, partial: true, size: 50 }).promise() if (results.hits.found < 5) { const suggestResults = await this.clientDomain.suggest({ query: q, suggester: 'default_suggester', size: 5 }).promise() suggestions = suggestResults.suggest.suggestions.map(s => s.suggestion) } return { results: _.map(results.hits.hit, r => ({ id: r.id, path: _.head(r.fields.path), locale: _.head(r.fields.locale), title: _.head(r.fields.title) || '', description: _.head(r.fields.description) || '' })), suggestions: suggestions, totalHits: results.hits.found } } catch (err) { WIKI.logger.warn('Search Engine Error:') WIKI.logger.warn(err) } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify([ { type: 'add', id: page.hash, fields: { locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent } } ]) }).promise() }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify([ { type: 'add', id: page.hash, fields: { locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent } } ]) }).promise() }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify([ { type: 'delete', id: page.hash } ]) }).promise() }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify([ { type: 'delete', id: page.hash } ]) }).promise() await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify([ { type: 'add', id: page.destinationHash, fields: { locale: page.destinationLocaleCode, path: page.destinationPath, title: page.title, description: page.description, content: page.safeContent } } ]) }).promise() }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/AWS) Rebuilding Index...`) const MAX_DOCUMENT_BYTES = Math.pow(2, 20) const MAX_INDEXING_BYTES = 5 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength const MAX_INDEXING_COUNT = 1000 const COMMA_BYTES = Buffer.from(',').byteLength let chunks = [] let bytes = 0 const processDocument = async (cb, doc) => { try { if (doc) { const docBytes = Buffer.from(JSON.stringify(doc)).byteLength // -> Document too large if (docBytes >= MAX_DOCUMENT_BYTES) { throw new Error('Document exceeds maximum size allowed by AWS CloudSearch.') } // -> Current batch exceeds size hard limit, flush if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) { await flushBuffer() } if (chunks.length > 0) { bytes += COMMA_BYTES } bytes += docBytes chunks.push(doc) // -> Current batch exceeds count soft limit, flush if (chunks.length >= MAX_INDEXING_COUNT) { await flushBuffer() } } else { // -> End of stream, flush await flushBuffer() } cb() } catch (err) { cb(err) } } const flushBuffer = async () => { WIKI.logger.info(`(SEARCH/AWS) Sending batch of ${chunks.length}...`) try { await this.clientDomain.uploadDocuments({ contentType: 'application/json', documents: JSON.stringify(_.map(chunks, doc => ({ type: 'add', id: doc.id, fields: { locale: doc.locale, path: doc.path, title: doc.title, description: doc.description, content: WIKI.models.pages.cleanHTML(doc.render) } }))) }).promise() } catch (err) { WIKI.logger.warn('(SEARCH/AWS) Failed to send batch to AWS CloudSearch: ', err) } chunks.length = 0 bytes = 0 } await pipeline( WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({ isPublished: true, isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (chunk, enc, cb) => processDocument(cb, chunk), flush: async (cb) => processDocument(cb) }) ) WIKI.logger.info(`(SEARCH/AWS) Requesting Index Rebuild...`) await this.client.indexDocuments({ DomainName: this.config.domain }).promise() WIKI.logger.info(`(SEARCH/AWS) Index rebuilt successfully.`) } } ================================================ FILE: server/modules/search/azure/definition.yml ================================================ key: azure title: Azure Search description: AI-Powered cloud search service for web and mobile app development. author: requarks.io logo: https://static.requarks.io/logo/azure.svg website: https://azure.microsoft.com/services/search/ isAvailable: true props: serviceName: type: String title: Service Name hint: The name of the Azure Search Service. Found under Properties. order: 1 adminKey: type: String title: Admin API Key hint: Either the primary or secondary admin key. Found under Keys. order: 2 indexName: type: String title: Index Name hint: 'Name to use when creating the index. (default: wiki)' default: wiki order: 3 ================================================ FILE: server/modules/search/azure/engine.js ================================================ const _ = require('lodash') const { SearchService, QueryType } = require('azure-search-client') const request = require('request-promise') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') /* global WIKI */ module.exports = { async activate() { // not used }, async deactivate() { // not used }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/AZURE) Initializing...`) this.client = new SearchService(this.config.serviceName, this.config.adminKey) // -> Create Search Index const indexes = await this.client.indexes.list() if (!_.find(_.get(indexes, 'result.value', []), ['name', this.config.indexName])) { WIKI.logger.info(`(SEARCH/AZURE) Creating index...`) await this.client.indexes.create({ name: this.config.indexName, fields: [ { name: 'id', type: 'Edm.String', key: true, searchable: false }, { name: 'locale', type: 'Edm.String', searchable: false }, { name: 'path', type: 'Edm.String', searchable: false }, { name: 'title', type: 'Edm.String', searchable: true }, { name: 'description', type: 'Edm.String', searchable: true }, { name: 'content', type: 'Edm.String', searchable: true } ], scoringProfiles: [ { name: 'fieldWeights', text: { weights: { title: 4, description: 3, content: 1 } } } ], suggesters: [ { name: 'suggestions', searchMode: 'analyzingInfixMatching', sourceFields: ['title', 'description', 'content'] } ] }) } WIKI.logger.info(`(SEARCH/AZURE) Initialization completed.`) }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { let suggestions = [] const results = await this.client.indexes.use(this.config.indexName).search({ count: true, scoringProfile: 'fieldWeights', search: q, select: 'id, locale, path, title, description', queryType: QueryType.simple, top: 50 }) if (results.result.value.length < 5) { // Using plain request, not yet available in library... try { const suggestResults = await request({ uri: `https://${this.config.serviceName}.search.windows.net/indexes/${this.config.indexName}/docs/autocomplete`, method: 'post', qs: { 'api-version': '2017-11-11-Preview' }, headers: { 'api-key': this.config.adminKey, 'Content-Type': 'application/json' }, json: true, body: { autocompleteMode: 'oneTermWithContext', search: q, suggesterName: 'suggestions' } }) suggestions = suggestResults.value.map(s => s.queryPlusText) } catch (err) { WIKI.logger.warn('Search Engine suggestion failure: ', err) } } return { results: results.result.value, suggestions, totalHits: results.result['@odata.count'] } } catch (err) { WIKI.logger.warn('Search Engine Error:') WIKI.logger.warn(err) } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await this.client.indexes.use(this.config.indexName).index([ { id: page.hash, locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent } ]) }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await this.client.indexes.use(this.config.indexName).index([ { id: page.hash, locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent } ]) }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await this.client.indexes.use(this.config.indexName).index([ { '@search.action': 'delete', id: page.hash } ]) }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await this.client.indexes.use(this.config.indexName).index([ { '@search.action': 'delete', id: page.hash } ]) await this.client.indexes.use(this.config.indexName).index([ { id: page.destinationHash, locale: page.destinationLocaleCode, path: page.destinationPath, title: page.title, description: page.description, content: page.safeContent } ]) }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/AZURE) Rebuilding Index...`) await pipeline( WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({ isPublished: true, isPrivate: false }).stream(), new Transform({ objectMode: true, transform: (chunk, enc, cb) => { cb(null, { id: chunk.id, path: chunk.path, locale: chunk.locale, title: chunk.title, description: chunk.description, content: WIKI.models.pages.cleanHTML(chunk.render) }) } }), this.client.indexes.use(this.config.indexName).createIndexingStream() ) WIKI.logger.info(`(SEARCH/AZURE) Index rebuilt successfully.`) } } ================================================ FILE: server/modules/search/db/definition.yml ================================================ key: db title: Database - Basic description: Default basic database-based search engine. author: requarks.io logo: https://static.requarks.io/logo/database.svg website: https://www.requarks.io/ isAvailable: true props: {} ================================================ FILE: server/modules/search/db/engine.js ================================================ /* global WIKI */ module.exports = { activate() { // not used }, deactivate() { // not used }, /** * INIT */ init() { // not used }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { const results = await WIKI.models.pages.query() .column('pages.id', 'title', 'description', 'path', 'localeCode as locale') .withGraphJoined('tags') // Adding page tags since they can be used to check resource access permissions .modifyGraph('tags', builder => { builder.select('tag') }) .where(builder => { builder.where('isPublished', true) if (opts.locale) { builder.andWhere('localeCode', opts.locale) } if (opts.path) { builder.andWhere('path', 'like', `${opts.path}%`) } builder.andWhere(builderSub => { if (WIKI.config.db.type === 'postgres') { builderSub.where('title', 'ILIKE', `%${q}%`) builderSub.orWhere('description', 'ILIKE', `%${q}%`) builderSub.orWhere('path', 'ILIKE', `%${q.toLowerCase()}%`) } else { builderSub.where('title', 'LIKE', `%${q}%`) builderSub.orWhere('description', 'LIKE', `%${q}%`) builderSub.orWhere('path', 'LIKE', `%${q.toLowerCase()}%`) } }) }) .limit(WIKI.config.search.maxHits) return { results, suggestions: [], totalHits: results.length } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { // not used }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { // not used }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { // not used }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { // not used }, /** * REBUILD INDEX */ async rebuild() { // not used } } ================================================ FILE: server/modules/search/elasticsearch/definition.yml ================================================ key: elasticsearch title: Elasticsearch description: Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. author: requarks.io logo: https://static.requarks.io/logo/elasticsearch.svg website: https://www.elastic.co/products/elasticsearch isAvailable: true props: apiVersion: type: String title: Elasticsearch Version hint: Should match the version of the Elasticsearch nodes you are connecting to order: 1 enum: - '8.x' - '7.x' - '6.x' default: '7.x' hosts: type: String title: Host(s) hint: Comma-separated list of Elasticsearch hosts to connect to, including the port, username and password if necessary. (e.g. http://localhost:9200, https://user:pass@es1.example.com:9200) order: 2 verifyTLSCertificate: title: Verify TLS Certificate type: Boolean default: true order: 3 tlsCertPath: title: TLS Certificate Path type: String hint: Absolute path to the TLS certificate on the server. order: 4 indexName: type: String title: Index Name hint: The index name to use during creation default: wiki order: 5 analyzer: type: String title: Analyzer hint: 'The token analyzer in elasticsearch' default: simple order: 6 sniffOnStart: type: Boolean title: Sniff on start hint: 'Should Wiki.js attempt to detect the rest of the cluster on first connect? (Default: off)' default: false order: 7 sniffInterval: type: Number title: Sniff Interval hint: '0 = disabled, Interval in seconds to check for updated list of nodes in cluster. (Default: 0)' default: 0 order: 8 ================================================ FILE: server/modules/search/elasticsearch/engine.js ================================================ const _ = require('lodash') const fs = require('fs') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') /* global WIKI */ module.exports = { async activate() { // not used }, async deactivate() { // not used }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Initializing...`) switch (this.config.apiVersion) { case '8.x': const { Client: Client8 } = require('elasticsearch8') this.client = new Client8({ nodes: this.config.hosts.split(',').map(_.trim), sniffOnStart: this.config.sniffOnStart, sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false, tls: getTlsOptions(this.config), name: 'wiki-js' }) break case '7.x': const { Client: Client7 } = require('elasticsearch7') this.client = new Client7({ nodes: this.config.hosts.split(',').map(_.trim), sniffOnStart: this.config.sniffOnStart, sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false, ssl: getTlsOptions(this.config), name: 'wiki-js' }) break case '6.x': const { Client: Client6 } = require('elasticsearch6') this.client = new Client6({ nodes: this.config.hosts.split(',').map(_.trim), sniffOnStart: this.config.sniffOnStart, sniffInterval: (this.config.sniffInterval > 0) ? this.config.sniffInterval : false, ssl: getTlsOptions(this.config), name: 'wiki-js' }) break default: throw new Error('Unsupported version of elasticsearch! Update your settings in the Administration Area.') } // -> Create Search Index await this.createIndex() WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Initialization completed.`) }, /** * Create Index */ async createIndex() { try { const indexExists = await this.client.indices.exists({ index: this.config.indexName }) // Elasticsearch 6.x / 7.x if (this.config.apiVersion !== '8.x' && !indexExists.body) { WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Creating index...`) try { const idxBody = { properties: { suggest: { type: 'completion' }, title: { type: 'text', boost: 10.0 }, description: { type: 'text', boost: 3.0 }, content: { type: 'text', boost: 1.0 }, locale: { type: 'keyword' }, path: { type: 'text' }, tags: { type: 'text', boost: 8.0 } } } await this.client.indices.create({ index: this.config.indexName, body: { mappings: (this.config.apiVersion === '6.x') ? { _doc: idxBody } : idxBody, settings: { analysis: { analyzer: { default: { type: this.config.analyzer } } } } } }) } catch (err) { WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Create Index Error: `, _.get(err, 'meta.body.error', err)) } // Elasticsearch 8.x } else if (this.config.apiVersion === '8.x' && !indexExists) { WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Creating index...`) try { // 8.x Doesn't support boost in mappings, so we will need to boost at query time. const idxBody = { properties: { suggest: { type: 'completion' }, title: { type: 'text' }, description: { type: 'text' }, content: { type: 'text' }, locale: { type: 'keyword' }, path: { type: 'text' }, tags: { type: 'text' } } } await this.client.indices.create({ index: this.config.indexName, body: { mappings: idxBody, settings: { analysis: { analyzer: { default: { type: this.config.analyzer } } } } } }) } catch (err) { WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Create Index Error: `, _.get(err, 'meta.body.error', err)) } } } catch (err) { WIKI.logger.error(`(SEARCH/ELASTICSEARCH) Index Check Error: `, _.get(err, 'meta.body.error', err)) } }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { const results = await this.client.search({ index: this.config.indexName, body: { query: { simple_query_string: { query: `*${q}*`, fields: ['title^20', 'description^3', 'tags^8', 'content^1'], default_operator: 'and', analyze_wildcard: true } }, from: 0, size: 50, _source: ['title', 'description', 'path', 'locale'], suggest: { suggestions: { text: q, completion: { field: 'suggest', size: 5, skip_duplicates: true, fuzzy: true } } } } }) return { results: _.get(results, this.config.apiVersion === '8.x' ? 'hits.hits' : 'body.hits.hits', []).map(r => ({ id: r._id, locale: r._source.locale, path: r._source.path, title: r._source.title, description: r._source.description })), suggestions: _.reject(_.get(results, 'suggest.suggestions', []).map(s => _.get(s, 'options[0].text', false)), s => !s), totalHits: _.get(results, this.config.apiVersion === '8.x' ? 'hits.total.value' : 'body.hits.total.value', _.get(results, this.config.apiVersion === '8.x' ? 'hits.total' : 'body.hits.total', 0)) } } catch (err) { WIKI.logger.warn('Search Engine Error: ', _.get(err, 'meta.body.error', err)) } }, /** * Build tags field * @param id * @returns {Promise<*|*[]>} */ async buildTags(id) { const tags = await WIKI.models.pages.query().findById(id).select('*').withGraphJoined('tags') return (tags.tags && tags.tags.length > 0) ? tags.tags.map(function (tag) { return tag.title }) : [] }, /** * Build suggest field */ buildSuggest(page) { return _.reject(_.uniq(_.concat( page.title.split(' ').map(s => ({ input: s, weight: 10 })), page.description.split(' ').map(s => ({ input: s, weight: 3 })), page.safeContent.split(' ').map(s => ({ input: s, weight: 1 })) )), ['input', '']) }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await this.client.index({ index: this.config.indexName, ...(this.config.apiVersion !== '8.x' && { type: '_doc' }), id: page.hash, body: { suggest: this.buildSuggest(page), locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent, tags: await this.buildTags(page.id) }, refresh: true }) }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await this.client.index({ index: this.config.indexName, ...(this.config.apiVersion !== '8.x' && { type: '_doc' }), id: page.hash, body: { suggest: this.buildSuggest(page), locale: page.localeCode, path: page.path, title: page.title, description: page.description, content: page.safeContent, tags: await this.buildTags(page.id) }, refresh: true }) }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await this.client.delete({ index: this.config.indexName, ...(this.config.apiVersion !== '8.x' && { type: '_doc' }), id: page.hash, refresh: true }) }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await this.client.delete({ index: this.config.indexName, ...(this.config.apiVersion !== '8.x' && { type: '_doc' }), id: page.hash, refresh: true }) await this.client.index({ index: this.config.indexName, ...(this.config.apiVersion !== '8.x' && { type: '_doc' }), id: page.destinationHash, body: { suggest: this.buildSuggest(page), locale: page.destinationLocaleCode, path: page.destinationPath, title: page.title, description: page.description, content: page.safeContent, tags: await this.buildTags(page.id) }, refresh: true }) }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Rebuilding Index...`) await this.client.indices.delete({ index: this.config.indexName }) await this.createIndex() const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB const MAX_INDEXING_COUNT = 1000 const COMMA_BYTES = Buffer.from(',').byteLength let chunks = [] let bytes = 0 const processDocument = async (cb, doc) => { try { if (doc) { const docBytes = Buffer.from(JSON.stringify(doc)).byteLength doc['tags'] = await this.buildTags(doc.realId) // -> Current batch exceeds size limit, flush if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) { await flushBuffer() } if (chunks.length > 0) { bytes += COMMA_BYTES } bytes += docBytes chunks.push(doc) // -> Current batch exceeds count limit, flush if (chunks.length >= MAX_INDEXING_COUNT) { await flushBuffer() } } else { // -> End of stream, flush await flushBuffer() } cb() } catch (err) { cb(err) } } const flushBuffer = async () => { WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Sending batch of ${chunks.length}...`) try { await this.client.bulk({ index: this.config.indexName, body: _.reduce(chunks, (result, doc) => { result.push({ index: { _index: this.config.indexName, _id: doc.id, ...(this.config.apiVersion !== '8.x' && { _type: '_doc' }) } }) doc.safeContent = WIKI.models.pages.cleanHTML(doc.render) result.push({ suggest: this.buildSuggest(doc), tags: doc.tags, locale: doc.locale, path: doc.path, title: doc.title, description: doc.description, content: doc.safeContent }) return result }, []), refresh: true }) } catch (err) { WIKI.logger.warn('(SEARCH/ELASTICSEARCH) Failed to send batch to elasticsearch: ', err) } chunks.length = 0 bytes = 0 } // Added real id in order to fetch page tags from the query await pipeline( WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render', { realId: 'id' }).select().from('pages').where({ isPublished: true, isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (chunk, enc, cb) => processDocument(cb, chunk), flush: async (cb) => processDocument(cb) }) ) WIKI.logger.info(`(SEARCH/ELASTICSEARCH) Index rebuilt successfully.`) } } function getTlsOptions(conf) { if (!conf.tlsCertPath) { return { rejectUnauthorized: conf.verifyTLSCertificate } } const caList = [] if (conf.verifyTLSCertificate) { caList.push(fs.readFileSync(conf.tlsCertPath)) } return { rejectUnauthorized: conf.verifyTLSCertificate, ca: caList } } ================================================ FILE: server/modules/search/manticore/definition.yml ================================================ key: manticore title: Manticore Search description: High performance full-text search engine with SQL and JSON support. author: requarks.io logo: https://static.requarks.io/logo/manticore.svg website: https://manticoresearch.com/ isAvailable: false props: {} ================================================ FILE: server/modules/search/manticore/engine.js ================================================ module.exports = { activate() { }, deactivate() { }, query() { }, created() { }, updated() { }, deleted() { }, renamed() { }, rebuild() { } } ================================================ FILE: server/modules/search/postgres/definition.yml ================================================ key: postgres title: Database - PostgreSQL description: Advanced PostgreSQL-based search engine. author: requarks.io logo: https://static.requarks.io/logo/postgresql.svg website: https://www.requarks.io/ isAvailable: true props: dictLanguage: type: String title: Dictionary Language hint: Language to use when creating and querying text search vectors. default: english enum: - simple - danish - dutch - english - finnish - french - german - hungarian - italian - norwegian - portuguese - romanian - russian - spanish - swedish - turkish order: 1 ================================================ FILE: server/modules/search/postgres/engine.js ================================================ const tsquery = require('pg-tsquery')() const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') /* global WIKI */ module.exports = { async activate() { if (WIKI.config.db.type !== 'postgres') { throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!') } }, async deactivate() { WIKI.logger.info(`(SEARCH/POSTGRES) Dropping index tables...`) await WIKI.models.knex.schema.dropTable('pagesWords') await WIKI.models.knex.schema.dropTable('pagesVector') WIKI.logger.info(`(SEARCH/POSTGRES) Index tables have been dropped.`) }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`) // -> Ensure pg_trgm extension is available (required for similarity search) await WIKI.models.knex.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm') // -> Create Search Index const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector') if (!indexExists) { WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`) await WIKI.models.knex.schema.createTable('pagesVector', table => { table.increments() table.string('path') table.string('locale') table.string('title') table.string('description') table.specificType('tokens', 'TSVECTOR') table.text('content') }) } // -> Create Words Index const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords') if (!wordsExists) { WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`) await WIKI.models.knex.raw(` CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat( 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"' )`) await WIKI.models.knex.raw(`CREATE INDEX "pageWords_idx" ON "pagesWords" USING GIN (word gin_trgm_ops)`) } WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`) }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { let suggestions = [] let qry = ` SELECT id, path, locale, title, description FROM "pagesVector", to_tsquery(?,?) query WHERE (query @@ "tokens" OR path ILIKE ?) ` let qryEnd = `ORDER BY ts_rank(tokens, query) DESC` let qryParams = [this.config.dictLanguage, tsquery(q), `%${q.toLowerCase()}%`] if (opts.locale) { qry = `${qry} AND locale = ?` qryParams.push(opts.locale) } if (opts.path) { qry = `${qry} AND path ILIKE ?` qryParams.push(`%${opts.path}`) } const results = await WIKI.models.knex.raw(` ${qry} ${qryEnd} `, qryParams) if (results.rows.length < 5) { try { const suggestResults = await WIKI.models.knex.raw(`SELECT word, word <-> ? AS rank FROM "pagesWords" WHERE similarity(word, ?) > 0.2 ORDER BY rank LIMIT 5;`, [q, q]) suggestions = suggestResults.rows.map(r => r.word) } catch (err) { WIKI.logger.warn(`Search Engine Suggestion Error (pg_trgm extension may be missing): ${err.message}`) } } return { results: results.rows, suggestions, totalHits: results.rows.length } } catch (err) { WIKI.logger.warn('Search Engine Error:') WIKI.logger.warn(err) } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await WIKI.models.knex.raw(` INSERT INTO "pagesVector" (path, locale, title, description, "tokens") VALUES ( ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C')) ) `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent]) }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await WIKI.models.knex.raw(` UPDATE "pagesVector" SET title = ?, description = ?, tokens = (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C')) WHERE path = ? AND locale = ? `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode]) }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await WIKI.models.knex('pagesVector').where({ locale: page.localeCode, path: page.path }).del().limit(1) }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await WIKI.models.knex('pagesVector').where({ locale: page.localeCode, path: page.path }).update({ locale: page.destinationLocaleCode, path: page.destinationPath }) }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`) await WIKI.models.knex('pagesVector').truncate() await WIKI.models.knex('pagesWords').truncate() await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'render').select().from('pages').where({ isPublished: true, isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const content = WIKI.models.pages.cleanHTML(page.render) await WIKI.models.knex.raw(` INSERT INTO "pagesVector" (path, locale, title, description, "tokens", content) VALUES ( ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C')), ? ) `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, content, content]) cb() } }) ) await WIKI.models.knex.raw(` INSERT INTO "pagesWords" (word) SELECT word FROM ts_stat( 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"' ) `) WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`) } } ================================================ FILE: server/modules/search/solr/definition.yml ================================================ key: solr title: Solr description: Solr is the popular, blazing-fast, open source enterprise search platform built on Apache Lucene. author: requarks.io logo: https://static.requarks.io/logo/solr.svg website: http://lucene.apache.org/solr/ isAvailable: false props: host: type: String title: Host hint: Host of the Solr server (e.g. 12.34.56.78 or solr.example.com) default: solr order: 1 port: type: Number title: Port hint: Port of the Solr server default: 8983 order: 2 core: type: String title: Core hint: Core name (e.g. wiki) default: wiki order: 3 protocol: type: String title: Protocol hint: Request protocol default: http enum: - http - https order: 4 ================================================ FILE: server/modules/search/solr/engine.js ================================================ module.exports = { activate() { }, deactivate() { }, query() { }, created() { }, updated() { }, deleted() { }, renamed() { }, rebuild() { } } ================================================ FILE: server/modules/search/sphinx/definition.yml ================================================ key: sphinx title: Sphinx description: Sphinx is an open source full text search server, designed from the ground up with performance, relevance and integration simplicity in mind. author: requarks.io logo: https://static.requarks.io/logo/sphinx.svg website: http://sphinxsearch.com/ isAvailable: false props: {} ================================================ FILE: server/modules/search/sphinx/engine.js ================================================ module.exports = { activate() { }, deactivate() { }, query() { }, created() { }, updated() { }, deleted() { }, renamed() { }, rebuild() { } } ================================================ FILE: server/modules/storage/azure/definition.yml ================================================ key: azure title: Azure Blob Storage description: Azure Blob Storage by Microsoft provides massively scalable object storage for unstructured data. author: requarks.io logo: https://static.requarks.io/logo/azure.svg website: https://azure.microsoft.com/services/storage/blobs/ isAvailable: true supportedModes: - push defaultMode: push schedule: false props: accountName: type: String title: Account Name default: '' hint: Your unique account name. order: 1 accountKey: type: String title: Account Access Key default: '' hint: Either key 1 or key 2. sensitive: true order: 2 containerName: type: String title: Container Name default: 'wiki' hint: Will automatically be created if it doesn't exist yet. order: 3 storageTier: type: String title: Storage Tier hint: Represents the access tier on a blob. Use Cool for lower storage costs but at higher retrieval costs. order: 4 default: 'Cool' enum: - 'Hot' - 'Cool' actions: - handler: exportAll label: Export All hint: Output all content from the DB to Azure Blob Storage, overwriting any existing data. If you enabled Azure Blob Storage after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content. ================================================ FILE: server/modules/storage/azure/storage.js ================================================ const { BlobServiceClient, StorageSharedKeyCredential } = require('@azure/storage-blob') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') const pageHelper = require('../../../helpers/page.js') const _ = require('lodash') /* global WIKI */ const getFilePath = (page, pathKey) => { const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}` const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName } module.exports = { async activated() { }, async deactivated() { }, async init() { WIKI.logger.info(`(STORAGE/AZURE) Initializing...`) const { accountName, accountKey, containerName } = this.config this.client = new BlobServiceClient( `https://${accountName}.blob.core.windows.net`, new StorageSharedKeyCredential(accountName, accountKey) ) this.container = this.client.getContainerClient(containerName) try { await this.container.create() } catch (err) { if (err.statusCode !== 409) { WIKI.logger.warn(err) throw err } } WIKI.logger.info(`(STORAGE/AZURE) Initialization completed.`) }, async created (page) { WIKI.logger.info(`(STORAGE/AZURE) Creating file ${page.path}...`) const filePath = getFilePath(page, 'path') const pageContent = page.injectMetadata() const blockBlobClient = this.container.getBlockBlobClient(filePath) await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier }) }, async updated (page) { WIKI.logger.info(`(STORAGE/AZURE) Updating file ${page.path}...`) const filePath = getFilePath(page, 'path') const pageContent = page.injectMetadata() const blockBlobClient = this.container.getBlockBlobClient(filePath) await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier }) }, async deleted (page) { WIKI.logger.info(`(STORAGE/AZURE) Deleting file ${page.path}...`) const filePath = getFilePath(page, 'path') const blockBlobClient = this.container.getBlockBlobClient(filePath) await blockBlobClient.delete({ deleteSnapshots: 'include' }) }, async renamed(page) { WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.path} to ${page.destinationPath}...`) let sourceFilePath = getFilePath(page, 'path') let destinationFilePath = getFilePath(page, 'destinationPath') if (WIKI.config.lang.namespacing) { if (WIKI.config.lang.code !== page.localeCode) { sourceFilePath = `${page.localeCode}/${sourceFilePath}` } if (WIKI.config.lang.code !== page.destinationLocaleCode) { destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}` } } const sourceBlockBlobClient = this.container.getBlockBlobClient(sourceFilePath) const destBlockBlobClient = this.container.getBlockBlobClient(destinationFilePath) await destBlockBlobClient.syncCopyFromURL(sourceBlockBlobClient.url) await sourceBlockBlobClient.delete({ deleteSnapshots: 'include' }) }, /** * ASSET UPLOAD * * @param {Object} asset Asset to upload */ async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/AZURE) Creating new file ${asset.path}...`) const blockBlobClient = this.container.getBlockBlobClient(asset.path) await blockBlobClient.upload(asset.data, asset.data.length, { tier: this.config.storageTier }) }, /** * ASSET DELETE * * @param {Object} asset Asset to delete */ async assetDeleted (asset) { WIKI.logger.info(`(STORAGE/AZURE) Deleting file ${asset.path}...`) const blockBlobClient = this.container.getBlockBlobClient(asset.path) await blockBlobClient.delete({ deleteSnapshots: 'include' }) }, /** * ASSET RENAME * * @param {Object} asset Asset to rename */ async assetRenamed (asset) { WIKI.logger.info(`(STORAGE/AZURE) Renaming file from ${asset.path} to ${asset.destinationPath}...`) const sourceBlockBlobClient = this.container.getBlockBlobClient(asset.path) const destBlockBlobClient = this.container.getBlockBlobClient(asset.destinationPath) await destBlockBlobClient.syncCopyFromURL(sourceBlockBlobClient.url) await sourceBlockBlobClient.delete({ deleteSnapshots: 'include' }) }, async getLocalLocation () { }, /** * HANDLERS */ async exportAll() { WIKI.logger.info(`(STORAGE/AZURE) Exporting all content to Azure Blob Storage...`) // -> Pages await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({ isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const filePath = getFilePath(page, 'path') WIKI.logger.info(`(STORAGE/AZURE) Adding page ${filePath}...`) const pageContent = pageHelper.injectPageMetadata(page) const blockBlobClient = this.container.getBlockBlobClient(filePath) await blockBlobClient.upload(pageContent, pageContent.length, { tier: this.config.storageTier }) cb() } }) ) // -> Assets const assetFolders = await WIKI.models.assetFolders.getAllPaths() await pipeline( WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`(STORAGE/AZURE) Adding asset ${filename}...`) const blockBlobClient = this.container.getBlockBlobClient(filename) await blockBlobClient.upload(asset.data, asset.data.length, { tier: this.config.storageTier }) cb() } }) ) WIKI.logger.info('(STORAGE/AZURE) All content has been pushed to Azure Blob Storage.') } } ================================================ FILE: server/modules/storage/box/definition.yml ================================================ key: box title: Box description: Box is a cloud content management and file sharing service for businesses. author: requarks.io logo: https://static.requarks.io/logo/box.svg website: https://www.box.com/platform props: clientId: String clientSecret: String rootFolder: String ================================================ FILE: server/modules/storage/box/storage.js ================================================ module.exports = { async activated() { }, async deactivated() { }, async init() { }, async created() { }, async updated() { }, async deleted() { }, async renamed() { }, async getLocalLocation () { } } ================================================ FILE: server/modules/storage/digitalocean/definition.yml ================================================ key: digitalocean title: DigitalOcean Spaces description: DigitalOcean provides developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces) and more. author: andrewsim logo: https://static.requarks.io/logo/digitalocean.svg website: https://www.digitalocean.com/products/spaces/ isAvailable: true supportedModes: - push defaultMode: push schedule: false props: endpoint: type: String title: Endpoint hint: The DigitalOcean spaces endpoint that has the form ${REGION}.digitaloceanspaces.com default: nyc3.digitaloceanspaces.com enum: - ams3.digitaloceanspaces.com - fra1.digitaloceanspaces.com - nyc3.digitaloceanspaces.com - sfo2.digitaloceanspaces.com - sfo3.digitaloceanspaces.com - sgp1.digitaloceanspaces.com - tor1.digitaloceanspaces.com order: 1 bucket: type: String title: Space Unique Name hint: The unique space name to create (e.g. wiki-johndoe) order: 2 accessKeyId: type: String title: Access Key ID hint: The Access Key (Generated in API > Tokens/Keys > Spaces access keys). order: 3 secretAccessKey : type: String title: Access Key Secret hint: The Access Key Secret for the Access Key ID you created above. sensitive: true order: 4 actions: - handler: exportAll label: Export All hint: Output all content from the DB to DigitalOcean Spaces, overwriting any existing data. If you enabled DigitalOcean Spaces after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content. ================================================ FILE: server/modules/storage/digitalocean/storage.js ================================================ const S3CompatibleStorage = require('../s3/common') module.exports = new S3CompatibleStorage('Digitalocean') ================================================ FILE: server/modules/storage/disk/common.js ================================================ const fs = require('fs-extra') const path = require('path') const { pipeline } = require('stream/promises') const { Transform } = require('stream') const klaw = require('klaw') const mime = require('mime-types').lookup const _ = require('lodash') const pageHelper = require('../../../helpers/page.js') /* global WIKI */ module.exports = { assetFolders: null, async importFromDisk ({ fullPath, moduleName }) { const rootUser = await WIKI.models.users.getRootUser() await pipeline( klaw(fullPath, { filter: (f) => { return !_.includes(f, '.git') } }), new Transform({ objectMode: true, transform: async (file, enc, cb) => { const relPath = file.path.substr(fullPath.length + 1) if (file.stats.size < 1) { // Skip directories and zero-byte files return cb() } else if (relPath && relPath.length > 3) { WIKI.logger.info(`(STORAGE/${moduleName}) Processing ${relPath}...`) const contentType = pageHelper.getContentType(relPath) if (contentType) { // -> Page try { await this.processPage({ user: rootUser, relPath: relPath, fullPath: fullPath, contentType: contentType, moduleName: moduleName }) } catch (err) { WIKI.logger.warn(`(STORAGE/${moduleName}) Failed to process page ${relPath}`) WIKI.logger.warn(err) } } else { // -> Asset try { await this.processAsset({ user: rootUser, relPath: relPath, file: file, contentType: contentType, moduleName: moduleName }) } catch (err) { WIKI.logger.warn(`(STORAGE/${moduleName}) Failed to process asset ${relPath}`) WIKI.logger.warn(err) } } } cb() } }) ) this.clearFolderCache() }, async processPage ({ user, fullPath, relPath, contentType, moduleName }) { const normalizedRelPath = relPath.replace(/\\/g, '/') const contentPath = pageHelper.getPagePath(normalizedRelPath) const itemContents = await fs.readFile(path.join(fullPath, relPath), 'utf8') const pageData = WIKI.models.pages.parseMetadata(itemContents, contentType) const currentPage = await WIKI.models.pages.getPageFromDb({ path: contentPath.path, locale: contentPath.locale }) const newTags = !_.isNil(pageData.tags) ? _.get(pageData, 'tags', '').split(', ') : false if (currentPage) { // Already in the DB, can mark as modified WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as modified: ${normalizedRelPath}`) await WIKI.models.pages.updatePage({ id: currentPage.id, title: _.get(pageData, 'title', currentPage.title), description: _.get(pageData, 'description', currentPage.description) || '', tags: newTags || currentPage.tags.map(t => t.tag), isPublished: _.get(pageData, 'isPublished', currentPage.isPublished), isPrivate: false, content: pageData.content, user: user, skipStorage: true }) } else { // Not in the DB, can mark as new WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as new: ${normalizedRelPath}`) const pageEditor = await WIKI.models.editors.getDefaultEditor(contentType) await WIKI.models.pages.createPage({ path: contentPath.path, locale: contentPath.locale, title: _.get(pageData, 'title', _.last(contentPath.path.split('/'))), description: _.get(pageData, 'description', '') || '', tags: newTags || [], isPublished: _.get(pageData, 'isPublished', true), isPrivate: false, content: pageData.content, user: user, editor: pageEditor, skipStorage: true }) } }, async processAsset ({ user, relPath, file, moduleName }) { WIKI.logger.info(`(STORAGE/${moduleName}) Asset marked for import: ${relPath}`) // -> Get all folder paths if (!this.assetFolders) { this.assetFolders = await WIKI.models.assetFolders.getAllPaths() } // -> Find existing folder const filePathInfo = path.parse(file.path) const folderPath = path.dirname(relPath).replace(/\\/g, '/') let folderId = _.toInteger(_.findKey(this.assetFolders, fld => { return fld === folderPath })) || null // -> Create missing folder structure if (!folderId && folderPath !== '.') { const folderParts = folderPath.split('/') let currentFolderPath = [] let currentFolderParentId = null for (const folderPart of folderParts) { currentFolderPath.push(folderPart) const existingFolderId = _.findKey(this.assetFolders, fld => { return fld === currentFolderPath.join('/') }) if (!existingFolderId) { const newFolderObj = await WIKI.models.assetFolders.query().insert({ slug: folderPart, name: folderPart, parentId: currentFolderParentId }) _.set(this.assetFolders, newFolderObj.id, currentFolderPath.join('/')) currentFolderParentId = newFolderObj.id } else { currentFolderParentId = _.toInteger(existingFolderId) } } folderId = currentFolderParentId } // -> Import asset await WIKI.models.assets.upload({ mode: 'import', originalname: filePathInfo.base, ext: filePathInfo.ext, mimetype: mime(filePathInfo.base) || 'application/octet-stream', size: file.stats.size, folderId: folderId, path: file.path, assetPath: relPath, user: user, skipStorage: true }) }, clearFolderCache () { this.assetFolders = null } } ================================================ FILE: server/modules/storage/disk/definition.yml ================================================ key: disk title: Local File System description: Local storage on disk or network shares. author: requarks.io logo: https://static.requarks.io/logo/local-fs.svg website: https://wiki.js.org isAvailable: true supportedModes: - push defaultMode: push schedule: false internalSchedule: P1D props: path: type: String title: Path hint: Absolute path without a trailing slash (e.g. /home/wiki/backup, C:\wiki\backup) order: 1 createDailyBackups: type: Boolean default: false title: Create Daily Backups hint: A tar.gz archive containing all content will be created daily in subfolder named _daily. Archives are kept for a month. order: 2 actions: - handler: dump label: Dump all content to disk hint: Output all content from the DB to the local disk. If you enabled this module after content was created or you temporarily disabled this module, you'll want to execute this action to add the missing files. - handler: backup label: Create Backup hint: Will create a manual backup archive at this point in time, in a subfolder named _manual, from the contents currently on disk. - handler: importAll label: Import Everything hint: Will import all content currently in the local disk folder. ================================================ FILE: server/modules/storage/disk/storage.js ================================================ const fs = require('fs-extra') const path = require('path') const tar = require('tar-fs') const zlib = require('zlib') const _ = require('lodash') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') const moment = require('moment') const pageHelper = require('../../../helpers/page') const commonDisk = require('./common') /* global WIKI */ module.exports = { async activated() { // not used }, async deactivated() { // not used }, async init() { WIKI.logger.info('(STORAGE/DISK) Initializing...') await fs.ensureDir(this.config.path) WIKI.logger.info('(STORAGE/DISK) Initialization completed.') }, async sync({ manual } = { manual: false }) { if (this.config.createDailyBackups || manual) { const dirPath = path.join(this.config.path, manual ? '_manual' : '_daily') await fs.ensureDir(dirPath) const dateFilename = moment().format(manual ? 'YYYYMMDD-HHmmss' : 'DD') WIKI.logger.info(`(STORAGE/DISK) Creating backup archive...`) await pipeline( tar.pack(this.config.path, { ignore: (filePath) => { return filePath.indexOf('_daily') >= 0 || filePath.indexOf('_manual') >= 0 } }), zlib.createGzip(), fs.createWriteStream(path.join(dirPath, `wiki-${dateFilename}.tar.gz`)) ) WIKI.logger.info('(STORAGE/DISK) Backup archive created successfully.') } }, async created(page) { WIKI.logger.info(`(STORAGE/DISK) Creating file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } const filePath = path.join(this.config.path, fileName) await fs.outputFile(filePath, page.injectMetadata(), 'utf8') }, async updated(page) { WIKI.logger.info(`(STORAGE/DISK) Updating file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } const filePath = path.join(this.config.path, fileName) await fs.outputFile(filePath, page.injectMetadata(), 'utf8') }, async deleted(page) { WIKI.logger.info(`(STORAGE/DISK) Deleting file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } const filePath = path.join(this.config.path, fileName) await fs.unlink(filePath) }, async renamed(page) { WIKI.logger.info(`(STORAGE/DISK) Renaming file [${page.localeCode}] ${page.path} to [${page.destinationLocaleCode}] ${page.destinationPath}...`) let sourceFilePath = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` let destinationFilePath = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.namespacing) { if (WIKI.config.lang.code !== page.localeCode) { sourceFilePath = `${page.localeCode}/${sourceFilePath}` } if (WIKI.config.lang.code !== page.destinationLocaleCode) { destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}` } } await fs.move(path.join(this.config.path, sourceFilePath), path.join(this.config.path, destinationFilePath), { overwrite: true }) }, /** * ASSET UPLOAD * * @param {Object} asset Asset to upload */ async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/DISK) Creating new file ${asset.path}...`) await fs.outputFile(path.join(this.config.path, asset.path), asset.data) }, /** * ASSET DELETE * * @param {Object} asset Asset to delete */ async assetDeleted (asset) { WIKI.logger.info(`(STORAGE/DISK) Deleting file ${asset.path}...`) await fs.remove(path.join(this.config.path, asset.path)) }, /** * ASSET RENAME * * @param {Object} asset Asset to rename */ async assetRenamed (asset) { WIKI.logger.info(`(STORAGE/DISK) Renaming file from ${asset.path} to ${asset.destinationPath}...`) await fs.move(path.join(this.config.path, asset.path), path.join(this.config.path, asset.destinationPath), { overwrite: true }) }, async getLocalLocation (asset) { return path.join(this.config.path, asset.path) }, /** * HANDLERS */ async dump() { WIKI.logger.info(`(STORAGE/DISK) Dumping all content to disk...`) // -> Pages await pipeline( WIKI.models.knex.column('id', 'path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt', 'editorKey').select().from('pages').where({ isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const pageObject = await WIKI.models.pages.query().findById(page.id) page.tags = await pageObject.$relatedQuery('tags') let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` } WIKI.logger.info(`(STORAGE/DISK) Dumping page ${fileName}...`) const filePath = path.join(this.config.path, fileName) await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8') cb() } }) ) // -> Assets const assetFolders = await WIKI.models.assetFolders.getAllPaths() await pipeline( WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`(STORAGE/DISK) Dumping asset ${filename}...`) await fs.outputFile(path.join(this.config.path, filename), asset.data) cb() } }) ) WIKI.logger.info('(STORAGE/DISK) All content was dumped to disk successfully.') }, async backup() { return this.sync({ manual: true }) }, async importAll() { WIKI.logger.info(`(STORAGE/DISK) Importing all content from local disk folder to the DB...`) await commonDisk.importFromDisk({ fullPath: this.config.path, moduleName: 'DISK' }) WIKI.logger.info('(STORAGE/DISK) Import completed.') } } ================================================ FILE: server/modules/storage/dropbox/definition.yml ================================================ key: dropbox title: Dropbox description: Dropbox is a file hosting service that offers cloud storage, file synchronization, personal cloud, and client software. author: requarks.io logo: https://static.requarks.io/logo/dropbox.svg website: https://dropbox.com props: appKey: String appSecret: String ================================================ FILE: server/modules/storage/dropbox/storage.js ================================================ module.exports = { async activated() { }, async deactivated() { }, async init() { }, async created() { }, async updated() { }, async deleted() { }, async renamed() { }, async getLocalLocation () { } } ================================================ FILE: server/modules/storage/gdrive/definition.yml ================================================ key: gdrive title: Google Drive description: Google Drive is a file storage and synchronization service developed by Google. author: requarks.io logo: https://static.requarks.io/logo/google-drive.svg website: https://www.google.com/drive/ props: clientId: String clientSecret: String ================================================ FILE: server/modules/storage/gdrive/storage.js ================================================ module.exports = { async activated() { }, async deactivated() { }, async init() { }, async created() { }, async updated() { }, async deleted() { }, async renamed() { }, async getLocalLocation () { } } ================================================ FILE: server/modules/storage/git/definition.yml ================================================ key: git title: Git description: Git is a version control system for tracking changes in computer files and coordinating work on those files among multiple people. author: requarks.io logo: https://static.requarks.io/logo/git-alt.svg website: https://git-scm.com/ isAvailable: true supportedModes: - sync - push - pull defaultMode: sync schedule: PT5M props: authType: type: String default: 'ssh' title: Authentication Type hint: Use SSH for maximum security. enum: - 'basic' - 'ssh' order: 1 repoUrl: type: String title: Repository URI hint: Git-compliant URI (e.g. git@github.com:org/repo.git for ssh, https://github.com/org/repo.git for basic) order: 2 branch: type: String default: 'master' hint: The branch to use during pull / push order: 3 sshPrivateKeyMode: type: String title: SSH Private Key Mode hint: SSH Authentication Only - The mode to use to load the private key. Fill in the corresponding field below. order: 11 default: 'path' enum: - 'path' - 'contents' sshPrivateKeyPath: type: String title: A - SSH Private Key Path hint: SSH Authentication Only - Absolute path to the key. The key must NOT be passphrase-protected. Mode must be set to path to use this option. order: 12 sshPrivateKeyContent: type: String title: B - SSH Private Key Contents hint: SSH Authentication Only - Paste the contents of the private key. The key must NOT be passphrase-protected. Mode must be set to contents to use this option. multiline: true sensitive: true order: 13 verifySSL: type: Boolean default: true title: Verify SSL Certificate hint: Some hosts requires SSL certificate checking to be disabled. Leave enabled for proper security. order: 14 basicUsername: type: String title: Username hint: Basic Authentication Only order: 20 basicPassword: type: String title: Password / PAT hint: Basic Authentication Only sensitive: true order: 21 defaultEmail: type: String title: Default Author Email default: 'name@company.com' hint: 'Used as fallback in case the author of the change is not present.' order: 22 defaultName: type: String title: Default Author Name default: 'John Smith' hint: 'Used as fallback in case the author of the change is not present.' order: 23 localRepoPath: type: String title: Local Repository Path default: './data/repo' hint: 'Path where the local git repository will be created.' order: 30 alwaysNamespace: type: Boolean title: Always Locale Namespace default: false hint: 'Whether to put content from the primary language into a subfolder.' order: 40 gitBinaryPath: type: String title: Git Binary Path default: '' hint: Optional - Absolute path to the Git binary, when not available in PATH. Leave empty to use the default PATH location (recommended). order: 50 actions: - handler: syncUntracked label: Add Untracked Changes hint: Output all content from the DB to the local Git repository to ensure all untracked content is saved. If you enabled Git after content was created or you temporarily disabled Git, you'll want to execute this action to add the missing untracked changes. - handler: sync label: Force Sync hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected. - handler: importAll label: Import Everything hint: Will import all content currently in the local Git repository, regardless of the latest commit state. Useful for importing content from the remote repository created before git was enabled. - handler: purge label: Purge Local Repository hint: If you have unrelated merge histories, clearing the local repository can resolve this issue. This will not affect the remote repository or perform any commit. ================================================ FILE: server/modules/storage/git/storage.js ================================================ const path = require('path') const sgit = require('simple-git') const fs = require('fs-extra') const _ = require('lodash') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') const klaw = require('klaw') const os = require('os') const pageHelper = require('../../../helpers/page') const assetHelper = require('../../../helpers/asset') const commonDisk = require('../disk/common') /* global WIKI */ module.exports = { git: null, repoPath: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'repo'), async activated() { // not used }, async deactivated() { // not used }, /** * INIT */ async init() { WIKI.logger.info('(STORAGE/GIT) Initializing...') this.repoPath = path.resolve(WIKI.ROOTPATH, this.config.localRepoPath) await fs.ensureDir(this.repoPath) this.git = sgit(this.repoPath, { maxConcurrentProcesses: 1 }) // Set custom binary path if (!_.isEmpty(this.config.gitBinaryPath)) { this.git.customBinary(this.config.gitBinaryPath) } // Initialize repo (if needed) WIKI.logger.info('(STORAGE/GIT) Checking repository state...') const isRepo = await this.git.checkIsRepo() if (!isRepo) { WIKI.logger.info('(STORAGE/GIT) Initializing local repository...') await this.git.init() } // Disable quotePath, color output // Link https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath await this.git.raw(['config', '--local', 'core.quotepath', false]) await this.git.raw(['config', '--local', 'color.ui', false]) // Set default author await this.git.raw(['config', '--local', 'user.email', this.config.defaultEmail]) await this.git.raw(['config', '--local', 'user.name', this.config.defaultName]) // Purge existing remotes WIKI.logger.info('(STORAGE/GIT) Listing existing remotes...') const remotes = await this.git.getRemotes() if (remotes.length > 0) { WIKI.logger.info('(STORAGE/GIT) Purging existing remotes...') for (let remote of remotes) { await this.git.removeRemote(remote.name) } } // Add remote WIKI.logger.info('(STORAGE/GIT) Setting SSL Verification config...') await this.git.raw(['config', '--local', '--bool', 'http.sslVerify', _.toString(this.config.verifySSL)]) switch (this.config.authType) { case 'ssh': WIKI.logger.info('(STORAGE/GIT) Setting SSH Command config...') if (this.config.sshPrivateKeyMode === 'contents') { try { this.config.sshPrivateKeyPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'secure/git-ssh.pem') await fs.outputFile(this.config.sshPrivateKeyPath, this.config.sshPrivateKeyContent + os.EOL, { encoding: 'utf8', mode: 0o600 }) } catch (err) { WIKI.logger.error(err) throw err } } await this.git.addConfig('core.sshCommand', `ssh -i "${this.config.sshPrivateKeyPath}" -o StrictHostKeyChecking=no`) WIKI.logger.info('(STORAGE/GIT) Adding origin remote via SSH...') await this.git.addRemote('origin', this.config.repoUrl) break default: WIKI.logger.info('(STORAGE/GIT) Adding origin remote via HTTP/S...') let originUrl = '' if (_.startsWith(this.config.repoUrl, 'http')) { originUrl = this.config.repoUrl.replace('://', `://${encodeURI(this.config.basicUsername)}:${encodeURI(this.config.basicPassword)}@`) } else { originUrl = `https://${encodeURI(this.config.basicUsername)}:${encodeURI(this.config.basicPassword)}@${this.config.repoUrl}` } await this.git.addRemote('origin', originUrl) break } // Fetch updates for remote WIKI.logger.info('(STORAGE/GIT) Fetch updates from remote...') await this.git.raw(['remote', 'update', 'origin']) // Checkout branch const branches = await this.git.branch() if (!_.includes(branches.all, this.config.branch) && !_.includes(branches.all, `remotes/origin/${this.config.branch}`)) { throw new Error('Invalid branch! Make sure it exists on the remote first.') } WIKI.logger.info(`(STORAGE/GIT) Checking out branch ${this.config.branch}...`) await this.git.checkout(this.config.branch) // Perform initial sync await this.sync() WIKI.logger.info('(STORAGE/GIT) Initialization completed.') }, /** * SYNC */ async sync() { const currentCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {}) const rootUser = await WIKI.models.users.getRootUser() // Pull rebase if (_.includes(['sync', 'pull'], this.mode)) { WIKI.logger.info(`(STORAGE/GIT) Performing pull rebase from origin on branch ${this.config.branch}...`) await this.git.pull('origin', this.config.branch, ['--rebase']) } // Push if (_.includes(['sync', 'push'], this.mode)) { WIKI.logger.info(`(STORAGE/GIT) Performing push to origin on branch ${this.config.branch}...`) let pushOpts = ['--signed=if-asked'] if (this.mode === 'push') { pushOpts.push('--force') } await this.git.push('origin', this.config.branch, pushOpts) } // Process Changes if (_.includes(['sync', 'pull'], this.mode)) { const latestCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {}) const diff = await this.git.diffSummary(['-M', currentCommitLog.hash, latestCommitLog.hash]) if (_.get(diff, 'files', []).length > 0) { let filesToProcess = [] const filePattern = /(.*?)(?:{(.*?))? => (?:(.*?)})?(.*)/ for (const f of diff.files) { const fMatch = f.file.match(filePattern) const fNames = { old: null, new: null } if (!fMatch) { fNames.old = f.file fNames.new = f.file } else if (!fMatch[2] && !fMatch[3]) { fNames.old = fMatch[1] fNames.new = fMatch[4] } else { fNames.old = (fMatch[1] + fMatch[2] + fMatch[4]).replace('//', '/') fNames.new = (fMatch[1] + fMatch[3] + fMatch[4]).replace('//', '/') } const fPath = path.join(this.repoPath, fNames.new) let fStats = { size: 0 } try { fStats = await fs.stat(fPath) } catch (err) { if (err.code !== 'ENOENT') { WIKI.logger.warn(`(STORAGE/GIT) Failed to access file ${f.file}! Skipping...`) continue } } filesToProcess.push({ ...f, file: { path: fPath, stats: fStats }, oldPath: fNames.old, relPath: fNames.new }) } await this.processFiles(filesToProcess, rootUser) } } }, /** * Process Files * * @param {Array} files Array of files to process */ async processFiles(files, user) { for (const item of files) { const contentType = pageHelper.getContentType(item.relPath) const fileExists = await fs.pathExists(item.file.path) if (!item.binary && contentType) { // -> Page if (fileExists && !item.importAll && item.relPath !== item.oldPath) { // Page was renamed by git, so rename in DB WIKI.logger.info(`(STORAGE/GIT) Page marked as renamed: from ${item.oldPath} to ${item.relPath}`) const contentPath = pageHelper.getPagePath(item.oldPath) const contentDestinationPath = pageHelper.getPagePath(item.relPath) await WIKI.models.pages.movePage({ user: user, path: contentPath.path, destinationPath: contentDestinationPath.path, locale: contentPath.locale, destinationLocale: contentPath.locale, skipStorage: true }) } else if (!fileExists && !item.importAll && item.deletions > 0 && item.insertions === 0) { // Page was deleted by git, can safely mark as deleted in DB WIKI.logger.info(`(STORAGE/GIT) Page marked as deleted: ${item.relPath}`) const contentPath = pageHelper.getPagePath(item.relPath) await WIKI.models.pages.deletePage({ user: user, path: contentPath.path, locale: contentPath.locale, skipStorage: true }) continue } try { await commonDisk.processPage({ user, relPath: item.relPath, fullPath: this.repoPath, contentType: contentType, moduleName: 'GIT' }) } catch (err) { WIKI.logger.warn(`(STORAGE/GIT) Failed to process ${item.relPath}`) WIKI.logger.warn(err) } } else { // -> Asset if (fileExists && !item.importAll && ((item.before === item.after) || (item.deletions === 0 && item.insertions === 0))) { // Asset was renamed by git, so rename in DB WIKI.logger.info(`(STORAGE/GIT) Asset marked as renamed: from ${item.oldPath} to ${item.relPath}`) const fileHash = assetHelper.generateHash(item.relPath) const assetToRename = await WIKI.models.assets.query().findOne({ hash: fileHash }) if (assetToRename) { await WIKI.models.assets.query().patch({ filename: item.relPath, hash: fileHash }).findById(assetToRename.id) await assetToRename.deleteAssetCache() } else { WIKI.logger.info(`(STORAGE/GIT) Asset was not found in the DB, nothing to rename: ${item.relPath}`) } continue } else if (!fileExists && !item.importAll && ((item.before > 0 && item.after === 0) || (item.deletions > 0 && item.insertions === 0))) { // Asset was deleted by git, can safely mark as deleted in DB WIKI.logger.info(`(STORAGE/GIT) Asset marked as deleted: ${item.relPath}`) const fileHash = assetHelper.generateHash(item.relPath) const assetToDelete = await WIKI.models.assets.query().findOne({ hash: fileHash }) if (assetToDelete) { await WIKI.models.knex('assetData').where('id', assetToDelete.id).del() await WIKI.models.assets.query().deleteById(assetToDelete.id) await assetToDelete.deleteAssetCache() } else { WIKI.logger.info(`(STORAGE/GIT) Asset was not found in the DB, nothing to delete: ${item.relPath}`) } continue } try { await commonDisk.processAsset({ user, relPath: item.relPath, file: item.file, contentType: contentType, moduleName: 'GIT' }) } catch (err) { WIKI.logger.warn(`(STORAGE/GIT) Failed to process asset ${item.relPath}`) WIKI.logger.warn(err) } } } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { WIKI.logger.info(`(STORAGE/GIT) Committing new file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) { fileName = `${page.localeCode}/${fileName}` } const filePath = path.join(this.repoPath, fileName) await fs.outputFile(filePath, page.injectMetadata(), 'utf8') const gitFilePath = `./${fileName}` if ((await this.git.checkIgnore(gitFilePath)).length === 0) { await this.git.add(gitFilePath) await this.git.commit(`docs: create ${page.path}`, fileName, { '--author': `"${page.authorName} <${page.authorEmail}>"` }) } }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { WIKI.logger.info(`(STORAGE/GIT) Committing updated file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) { fileName = `${page.localeCode}/${fileName}` } const filePath = path.join(this.repoPath, fileName) await fs.outputFile(filePath, page.injectMetadata(), 'utf8') const gitFilePath = `./${fileName}` if ((await this.git.checkIgnore(gitFilePath)).length === 0) { await this.git.add(gitFilePath) await this.git.commit(`docs: update ${page.path}`, fileName, { '--author': `"${page.authorName} <${page.authorEmail}>"` }) } }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { WIKI.logger.info(`(STORAGE/GIT) Committing removed file [${page.localeCode}] ${page.path}...`) let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) { fileName = `${page.localeCode}/${fileName}` } const gitFilePath = `./${fileName}` if ((await this.git.checkIgnore(gitFilePath)).length === 0) { await this.git.rm(gitFilePath) await this.git.commit(`docs: delete ${page.path}`, fileName, { '--author': `"${page.authorName} <${page.authorEmail}>"` }) } }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { WIKI.logger.info(`(STORAGE/GIT) Committing file move from [${page.localeCode}] ${page.path} to [${page.destinationLocaleCode}] ${page.destinationPath}...`) let sourceFileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` let destinationFileName = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}` if (this.config.alwaysNamespace || WIKI.config.lang.namespacing) { if (this.config.alwaysNamespace || WIKI.config.lang.code !== page.localeCode) { sourceFileName = `${page.localeCode}/${sourceFileName}` } if (this.config.alwaysNamespace || WIKI.config.lang.code !== page.destinationLocaleCode) { destinationFileName = `${page.destinationLocaleCode}/${destinationFileName}` } } const sourceFilePath = path.join(this.repoPath, sourceFileName) const destinationFilePath = path.join(this.repoPath, destinationFileName) await fs.move(sourceFilePath, destinationFilePath) await this.git.rm(`./${sourceFileName}`) await this.git.add(`./${destinationFileName}`) await this.git.commit(`docs: rename ${page.path} to ${page.destinationPath}`, [sourceFilePath, destinationFilePath], { '--author': `"${page.moveAuthorName} <${page.moveAuthorEmail}>"` }) }, /** * ASSET UPLOAD * * @param {Object} asset Asset to upload */ async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/GIT) Committing new file ${asset.path}...`) const filePath = path.join(this.repoPath, asset.path) await fs.outputFile(filePath, asset.data, 'utf8') await this.git.add(`./${asset.path}`) await this.git.commit(`docs: upload ${asset.path}`, asset.path, { '--author': `"${asset.authorName} <${asset.authorEmail}>"` }) }, /** * ASSET DELETE * * @param {Object} asset Asset to upload */ async assetDeleted (asset) { WIKI.logger.info(`(STORAGE/GIT) Committing removed file ${asset.path}...`) await this.git.rm(`./${asset.path}`) await this.git.commit(`docs: delete ${asset.path}`, asset.path, { '--author': `"${asset.authorName} <${asset.authorEmail}>"` }) }, /** * ASSET RENAME * * @param {Object} asset Asset to upload */ async assetRenamed (asset) { WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${asset.path} to ${asset.destinationPath}...`) await this.git.mv(`./${asset.path}`, `./${asset.destinationPath}`) await this.git.commit(`docs: rename ${asset.path} to ${asset.destinationPath}`, [asset.path, asset.destinationPath], { '--author': `"${asset.moveAuthorName} <${asset.moveAuthorEmail}>"` }) }, async getLocalLocation (asset) { return path.join(this.repoPath, asset.path) }, /** * HANDLERS */ async importAll() { WIKI.logger.info(`(STORAGE/GIT) Importing all content from local Git repo to the DB...`) const rootUser = await WIKI.models.users.getRootUser() await pipeline( klaw(this.repoPath, { filter: (f) => { return !_.includes(f, '.git') } }), new Transform({ objectMode: true, transform: async (file, enc, cb) => { const relPath = file.path.substr(this.repoPath.length + 1) if (file.stats.size < 1) { // Skip directories and zero-byte files return cb() } else if (relPath && relPath.length > 3) { WIKI.logger.info(`(STORAGE/GIT) Processing ${relPath}...`) await this.processFiles([{ user: rootUser, relPath, file, deletions: 0, insertions: 0, importAll: true }], rootUser) } cb() } }) ) commonDisk.clearFolderCache() WIKI.logger.info('(STORAGE/GIT) Import completed.') }, async syncUntracked() { WIKI.logger.info(`(STORAGE/GIT) Adding all untracked content...`) // -> Pages await pipeline( WIKI.models.knex.column('id', 'path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt', 'editorKey').select().from('pages').where({ isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const pageObject = await WIKI.models.pages.query().findById(page.id) page.tags = await pageObject.$relatedQuery('tags') let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (this.config.alwaysNamespace || (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode)) { fileName = `${page.localeCode}/${fileName}` } WIKI.logger.info(`(STORAGE/GIT) Adding page ${fileName}...`) const filePath = path.join(this.repoPath, fileName) await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8') await this.git.add(`./${fileName}`) cb() } }) ) // -> Assets const assetFolders = await WIKI.models.assetFolders.getAllPaths() await pipeline( WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`(STORAGE/GIT) Adding asset ${filename}...`) await fs.outputFile(path.join(this.repoPath, filename), asset.data) await this.git.add(`./${filename}`) cb() } }) ) await this.git.commit(`docs: add all untracked content`) WIKI.logger.info('(STORAGE/GIT) All content is now tracked.') }, async purge() { WIKI.logger.info(`(STORAGE/GIT) Purging local repository...`) await fs.emptyDir(this.repoPath) WIKI.logger.info('(STORAGE/GIT) Local repository is now empty. Reinitializing...') await this.init() } } ================================================ FILE: server/modules/storage/onedrive/definition.yml ================================================ key: onedrive title: OneDrive description: OneDrive is a file hosting service operated by Microsoft as part of its suite of Office Online services. author: requarks.io logo: https://static.requarks.io/logo/onedrive.svg website: https://onedrive.live.com/about/ props: clientId: String clientSecret: String ================================================ FILE: server/modules/storage/onedrive/storage.js ================================================ module.exports = { async activated() { }, async deactivated() { }, async init() { }, async created() { }, async updated() { }, async deleted() { }, async renamed() { }, async getLocalLocation () { } } ================================================ FILE: server/modules/storage/s3/common.js ================================================ const S3 = require('aws-sdk/clients/s3') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') const _ = require('lodash') const pageHelper = require('../../../helpers/page.js') /* global WIKI */ /** * Deduce the file path given the `page` object and the object's key to the page's path. */ const getFilePath = (page, pathKey) => { const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}` const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName } /** * Can be used with S3 compatible storage. */ module.exports = class S3CompatibleStorage { constructor(storageName) { this.storageName = storageName this.bucketName = '' } async activated() { // not used } async deactivated() { // not used } async init() { WIKI.logger.info(`(STORAGE/${this.storageName}) Initializing...`) const { accessKeyId, secretAccessKey, bucket } = this.config const s3Config = { accessKeyId, secretAccessKey, params: { Bucket: bucket }, apiVersions: '2006-03-01' } if (!_.isNil(this.config.region)) { s3Config.region = this.config.region } if (!_.isNil(this.config.endpoint)) { s3Config.endpoint = this.config.endpoint } if (!_.isNil(this.config.sslEnabled)) { s3Config.sslEnabled = this.config.sslEnabled } if (!_.isNil(this.config.s3ForcePathStyle)) { s3Config.s3ForcePathStyle = this.config.s3ForcePathStyle } if (!_.isNil(this.config.s3BucketEndpoint)) { s3Config.s3BucketEndpoint = this.config.s3BucketEndpoint } this.s3 = new S3(s3Config) this.bucketName = bucket // determine if a bucket exists and you have permission to access it await this.s3.headBucket().promise() WIKI.logger.info(`(STORAGE/${this.storageName}) Initialization completed.`) } async created(page) { WIKI.logger.info(`(STORAGE/${this.storageName}) Creating file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.s3.putObject({ Key: filePath, Body: page.injectMetadata() }).promise() } async updated(page) { WIKI.logger.info(`(STORAGE/${this.storageName}) Updating file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.s3.putObject({ Key: filePath, Body: page.injectMetadata() }).promise() } async deleted(page) { WIKI.logger.info(`(STORAGE/${this.storageName}) Deleting file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.s3.deleteObject({ Key: filePath }).promise() } async renamed(page) { WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.path} to ${page.destinationPath}...`) let sourceFilePath = getFilePath(page, 'path') let destinationFilePath = getFilePath(page, 'destinationPath') if (WIKI.config.lang.namespacing) { if (WIKI.config.lang.code !== page.localeCode) { sourceFilePath = `${page.localeCode}/${sourceFilePath}` } if (WIKI.config.lang.code !== page.destinationLocaleCode) { destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}` } } await this.s3.copyObject({ CopySource: `${this.bucketName}/${sourceFilePath}`, Key: destinationFilePath }).promise() await this.s3.deleteObject({ Key: sourceFilePath }).promise() } /** * ASSET UPLOAD * * @param {Object} asset Asset to upload */ async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/${this.storageName}) Creating new file ${asset.path}...`) await this.s3.putObject({ Key: asset.path, Body: asset.data }).promise() } /** * ASSET DELETE * * @param {Object} asset Asset to delete */ async assetDeleted (asset) { WIKI.logger.info(`(STORAGE/${this.storageName}) Deleting file ${asset.path}...`) await this.s3.deleteObject({ Key: asset.path }).promise() } /** * ASSET RENAME * * @param {Object} asset Asset to rename */ async assetRenamed (asset) { WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file from ${asset.path} to ${asset.destinationPath}...`) await this.s3.copyObject({ CopySource: `${this.bucketName}/${asset.path}`, Key: asset.destinationPath }).promise() await this.s3.deleteObject({ Key: asset.path }).promise() } async getLocalLocation () { } /** * HANDLERS */ async exportAll() { WIKI.logger.info(`(STORAGE/${this.storageName}) Exporting all content to the cloud provider...`) // -> Pages await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({ isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const filePath = getFilePath(page, 'path') WIKI.logger.info(`(STORAGE/${this.storageName}) Adding page ${filePath}...`) await this.s3.putObject({ Key: filePath, Body: pageHelper.injectPageMetadata(page) }).promise() cb() } }) ) // -> Assets const assetFolders = await WIKI.models.assetFolders.getAllPaths() await pipeline( WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`(STORAGE/${this.storageName}) Adding asset ${filename}...`) await this.s3.putObject({ Key: filename, Body: asset.data }).promise() cb() } }) ) WIKI.logger.info(`(STORAGE/${this.storageName}) All content has been pushed to the cloud provider.`) } } ================================================ FILE: server/modules/storage/s3/definition.yml ================================================ key: s3 title: Amazon S3 description: Amazon S3 is a cloud computing web service offered by Amazon Web Services which provides object storage. author: andrewsim logo: https://static.requarks.io/logo/aws-s3.svg website: https://aws.amazon.com/s3/ isAvailable: true supportedModes: - push defaultMode: push schedule: false props: region: type: String title: Region hint: The AWS datacenter region where the bucket will be created. order: 1 bucket: type: String title: Unique bucket name hint: The unique bucket name to create (e.g. wiki-johndoe). order: 2 accessKeyId: type: String title: Access Key ID hint: The Access Key. order: 3 secretAccessKey: type: String title: Secret Access Key hint: The Secret Access Key for the Access Key ID you created above. sensitive: true order: 4 actions: - handler: exportAll label: Export All hint: Output all content from the DB to S3, overwriting any existing data. If you enabled S3 after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content. ================================================ FILE: server/modules/storage/s3/storage.js ================================================ const S3CompatibleStorage = require('./common') module.exports = new S3CompatibleStorage('S3') ================================================ FILE: server/modules/storage/s3generic/definition.yml ================================================ key: s3generic title: S3 Generic description: Generic storage module for S3-compatible services. author: requarks.io logo: https://static.requarks.io/logo/aws-s3-alt.svg website: https://wiki.js.org isAvailable: true supportedModes: - push defaultMode: push schedule: false props: endpoint: type: String title: Endpoint URI hint: The full S3-compliant endpoint URI. default: https://service.region.example.com order: 1 bucket: type: String title: Unique bucket name hint: The unique bucket name to create (e.g. wiki-johndoe) order: 2 accessKeyId: type: String title: Access Key ID hint: The Access Key ID. order: 3 secretAccessKey: type: String title: Access Key Secret hint: The Access Key Secret for the Access Key ID above. sensitive: true order: 4 sslEnabled: type: Boolean title: Use SSL hint: Whether to enable SSL for requests default: true order: 5 s3ForcePathStyle: type: Boolean title: Force Path Style for S3 objects hint: Whether to force path style URLs for S3 objects. default: false order: 6 s3BucketEndpoint: type: Boolean title: Single Bucket Endpoint hint: Whether the provided endpoint addresses an individual bucket. default: false order: 7 actions: - handler: exportAll label: Export All hint: Output all content from the DB to the external service, overwriting any existing data. If you enabled this module after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content. ================================================ FILE: server/modules/storage/s3generic/storage.js ================================================ const S3CompatibleStorage = require('../s3/common') module.exports = new S3CompatibleStorage('S3Generic') ================================================ FILE: server/modules/storage/sftp/definition.yml ================================================ key: sftp title: SFTP description: SFTP (SSH File Transfer Protocol) is a secure file transfer protocol. It runs over the SSH protocol. It supports the full security and authentication functionality of SSH. author: requarks.io logo: https://static.requarks.io/logo/ssh.svg website: https://www.ssh.com/ssh/sftp isAvailable: true supportedModes: - push defaultMode: push schedule: false props: host: type: String title: Host default: '' hint: Hostname or IP of the remote SSH server. order: 1 port: type: Number title: Port default: 22 hint: SSH port of the remote server. order: 2 authMode: type: String title: Authentication Method default: 'privateKey' hint: Whether to use Private Key or Password-based authentication. A private key is highly recommended for best security. enum: - privateKey - password order: 3 username: type: String title: Username default: '' hint: Username for authentication. order: 4 privateKey: type: String title: Private Key Contents default: '' hint: (Private Key Authentication Only) - Contents of the private key multiline: true sensitive: true order: 5 passphrase: type: String title: Private Key Passphrase default: '' hint: (Private Key Authentication Only) - Passphrase if the private key is encrypted, leave empty otherwise sensitive: true order: 6 password: type: String title: Password default: '' hint: (Password-based Authentication Only) - Password for authentication sensitive: true order: 6 basePath: type: String title: Base Directory Path default: '/root/wiki' hint: Base directory where files will be transferred to. The path must already exists and be writable by the user. actions: - handler: exportAll label: Export All hint: Output all content from the DB to the remote SSH server, overwriting any existing data. If you enabled SFTP after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content. ================================================ FILE: server/modules/storage/sftp/storage.js ================================================ const SSH2Promise = require('ssh2-promise') const _ = require('lodash') const path = require('path') const { pipeline } = require('node:stream/promises') const { Transform } = require('node:stream') const pageHelper = require('../../../helpers/page.js') /* global WIKI */ const getFilePath = (page, pathKey) => { const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}` const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName } module.exports = { client: null, sftp: null, async activated() { }, async deactivated() { }, async init() { WIKI.logger.info(`(STORAGE/SFTP) Initializing...`) this.client = new SSH2Promise({ host: this.config.host, port: this.config.port || 22, username: this.config.username, password: (this.config.authMode === 'password') ? this.config.password : null, privateKey: (this.config.authMode === 'privateKey') ? this.config.privateKey : null, passphrase: (this.config.authMode === 'privateKey') ? this.config.passphrase : null }) await this.client.connect() this.sftp = this.client.sftp() try { await this.sftp.readdir(this.config.basePath) } catch (err) { WIKI.logger.warn(`(STORAGE/SFTP) ${err.message}`) throw new Error(`Unable to read specified base directory: ${err.message}`) } WIKI.logger.info(`(STORAGE/SFTP) Initialization completed.`) }, async created(page) { WIKI.logger.info(`(STORAGE/SFTP) Creating file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.ensureDirectory(filePath) await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata()) }, async updated(page) { WIKI.logger.info(`(STORAGE/SFTP) Updating file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.ensureDirectory(filePath) await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), page.injectMetadata()) }, async deleted(page) { WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${page.path}...`) const filePath = getFilePath(page, 'path') await this.sftp.unlink(path.posix.join(this.config.basePath, filePath)) }, async renamed(page) { WIKI.logger.info(`(STORAGE/SFTP) Renaming file ${page.path} to ${page.destinationPath}...`) let sourceFilePath = getFilePath(page, 'path') let destinationFilePath = getFilePath(page, 'destinationPath') if (WIKI.config.lang.namespacing) { if (WIKI.config.lang.code !== page.localeCode) { sourceFilePath = `${page.localeCode}/${sourceFilePath}` } if (WIKI.config.lang.code !== page.destinationLocaleCode) { destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}` } } await this.ensureDirectory(destinationFilePath) await this.sftp.rename(path.posix.join(this.config.basePath, sourceFilePath), path.posix.join(this.config.basePath, destinationFilePath)) }, /** * ASSET UPLOAD * * @param {Object} asset Asset to upload */ async assetUploaded (asset) { WIKI.logger.info(`(STORAGE/SFTP) Creating new file ${asset.path}...`) await this.ensureDirectory(asset.path) await this.sftp.writeFile(path.posix.join(this.config.basePath, asset.path), asset.data) }, /** * ASSET DELETE * * @param {Object} asset Asset to delete */ async assetDeleted (asset) { WIKI.logger.info(`(STORAGE/SFTP) Deleting file ${asset.path}...`) await this.sftp.unlink(path.posix.join(this.config.basePath, asset.path)) }, /** * ASSET RENAME * * @param {Object} asset Asset to rename */ async assetRenamed (asset) { WIKI.logger.info(`(STORAGE/SFTP) Renaming file from ${asset.path} to ${asset.destinationPath}...`) await this.ensureDirectory(asset.destinationPath) await this.sftp.rename(path.posix.join(this.config.basePath, asset.path), path.posix.join(this.config.basePath, asset.destinationPath)) }, async getLocalLocation () { }, /** * HANDLERS */ async exportAll() { WIKI.logger.info(`(STORAGE/SFTP) Exporting all content to the remote server...`) // -> Pages await pipeline( WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt', 'createdAt').select().from('pages').where({ isPrivate: false }).stream(), new Transform({ objectMode: true, transform: async (page, enc, cb) => { const filePath = getFilePath(page, 'path') WIKI.logger.info(`(STORAGE/SFTP) Adding page ${filePath}...`) await this.ensureDirectory(filePath) await this.sftp.writeFile(path.posix.join(this.config.basePath, filePath), pageHelper.injectPageMetadata(page)) cb() } }) ) // -> Assets const assetFolders = await WIKI.models.assetFolders.getAllPaths() await pipeline( WIKI.models.knex.column('filename', 'folderId', 'data').select().from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(), new Transform({ objectMode: true, transform: async (asset, enc, cb) => { const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename WIKI.logger.info(`(STORAGE/SFTP) Adding asset ${filename}...`) await this.ensureDirectory(filename) await this.sftp.writeFile(path.posix.join(this.config.basePath, filename), asset.data) cb() } }) ) WIKI.logger.info('(STORAGE/SFTP) All content has been pushed to the remote server.') }, async ensureDirectory(filePath) { if (filePath.indexOf('/') >= 0) { try { const folderPaths = _.dropRight(filePath.split('/')) for (let i = 1; i <= folderPaths.length; i++) { const folderSection = _.take(folderPaths, i).join('/') const folderDir = path.posix.join(this.config.basePath, folderSection) try { await this.sftp.readdir(folderDir) } catch (err) { await this.sftp.mkdir(folderDir) } } } catch (err) {} } } } ================================================ FILE: server/setup.js ================================================ const path = require('path') const { v4: uuid } = require('uuid') const bodyParser = require('body-parser') const compression = require('compression') const express = require('express') const favicon = require('serve-favicon') const http = require('http') const fs = require('fs-extra') const _ = require('lodash') const crypto = require('crypto') const pem2jwk = require('pem-jwk').pem2jwk const semver = require('semver') const randomBytesAsync = require('util').promisify(crypto.randomBytes) /* global WIKI */ module.exports = () => { WIKI.config.site = { path: '', title: 'Wiki.js' } WIKI.system = require('./core/system') // ---------------------------------------- // Define Express App // ---------------------------------------- let app = express() app.use(compression()) // ---------------------------------------- // Public Assets // ---------------------------------------- app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico'))) app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets'))) // ---------------------------------------- // View Engine Setup // ---------------------------------------- app.set('views', path.join(WIKI.SERVERPATH, 'views')) app.set('view engine', 'pug') app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) app.locals.config = WIKI.config app.locals.data = WIKI.data app.locals._ = require('lodash') app.locals.devMode = WIKI.devMode // ---------------------------------------- // HMR (Dev Mode Only) // ---------------------------------------- if (global.DEV) { app.use(global.WP_DEV.devMiddleware) app.use(global.WP_DEV.hotMiddleware) } // ---------------------------------------- // Controllers // ---------------------------------------- app.get('*', async (req, res) => { let packageObj = await fs.readJson(path.join(WIKI.ROOTPATH, 'package.json')) res.render('setup', { packageObj }) }) /** * Finalize */ app.post('/finalize', async (req, res) => { try { // Set config _.set(WIKI.config, 'auth', { audience: 'urn:wiki.js', tokenExpiration: '30m', tokenRenewal: '14d' }) _.set(WIKI.config, 'company', '') _.set(WIKI.config, 'features', { featurePageRatings: true, featurePageComments: true, featurePersonalWikis: true }) _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io') _.set(WIKI.config, 'host', req.body.siteUrl) _.set(WIKI.config, 'lang', { code: 'en', autoUpdate: true, namespacing: false, namespaces: [] }) _.set(WIKI.config, 'logo', { hasLogo: false, logoIsSquare: false }) _.set(WIKI.config, 'mail', { senderName: '', senderEmail: '', host: '', port: 465, name: '', secure: true, verifySSL: true, user: '', pass: '', useDKIM: false, dkimDomainName: '', dkimKeySelector: '', dkimPrivateKey: '' }) _.set(WIKI.config, 'seo', { description: '', robots: ['index', 'follow'], analyticsService: '', analyticsId: '' }) _.set(WIKI.config, 'sessionSecret', (await randomBytesAsync(32)).toString('hex')) _.set(WIKI.config, 'telemetry', { isEnabled: req.body.telemetry === true, clientId: uuid() }) _.set(WIKI.config, 'theming', { theme: 'default', darkMode: false, iconset: 'mdi', injectCSS: '', injectHead: '', injectBody: '' }) _.set(WIKI.config, 'title', 'Wiki.js') // Init Telemetry WIKI.kernel.initTelemetry() // WIKI.telemetry.sendEvent('setup', 'install-start') // Basic checks if (!semver.satisfies(process.version, '>=10.12')) { throw new Error('Node.js 10.12.x or later required!') } // Create directory structure WIKI.logger.info('Creating data directories...') await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath)) await fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache')) await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads')) // Generate certificates WIKI.logger.info('Generating certificates...') const certs = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem', cipher: 'aes-256-cbc', passphrase: WIKI.config.sessionSecret } }) _.set(WIKI.config, 'certs', { jwk: pem2jwk(certs.publicKey), public: certs.publicKey, private: certs.privateKey }) // Save config to DB WIKI.logger.info('Persisting config to DB...') await WIKI.configSvc.saveToDb([ 'auth', 'certs', 'company', 'features', 'graphEndpoint', 'host', 'lang', 'logo', 'mail', 'seo', 'sessionSecret', 'telemetry', 'theming', 'uploads', 'title' ], false) // Truncate tables (reset from previous failed install) await WIKI.models.locales.query().where('code', '!=', 'x').del() await WIKI.models.navigation.query().truncate() switch (WIKI.config.db.type) { case 'postgres': await WIKI.models.knex.raw('TRUNCATE groups, users CASCADE') break case 'mysql': case 'mariadb': await WIKI.models.groups.query().where('id', '>', 0).del() await WIKI.models.users.query().where('id', '>', 0).del() await WIKI.models.knex.raw('ALTER TABLE `groups` AUTO_INCREMENT = 1') await WIKI.models.knex.raw('ALTER TABLE `users` AUTO_INCREMENT = 1') break case 'mssql': await WIKI.models.groups.query().del() await WIKI.models.users.query().del() await WIKI.models.knex.raw(` IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'groups' AND last_value IS NOT NULL) DBCC CHECKIDENT ([groups], RESEED, 0) `) await WIKI.models.knex.raw(` IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'users' AND last_value IS NOT NULL) DBCC CHECKIDENT ([users], RESEED, 0) `) break case 'sqlite': await WIKI.models.groups.query().truncate() await WIKI.models.users.query().truncate() break } // Create default locale WIKI.logger.info('Installing default locale...') await WIKI.models.locales.query().insert({ code: 'en', strings: {}, isRTL: false, name: 'English', nativeName: 'English' }) // Create default groups WIKI.logger.info('Creating default groups...') const adminGroup = await WIKI.models.groups.query().insert({ name: 'Administrators', permissions: JSON.stringify(['manage:system']), pageRules: JSON.stringify([]), isSystem: true }) const guestGroup = await WIKI.models.groups.query().insert({ name: 'Guests', permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']), pageRules: JSON.stringify([ { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] } ]), isSystem: true }) if (adminGroup.id !== 1 || guestGroup.id !== 2) { throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.') } // Load local authentication strategy await WIKI.models.authentication.query().insert({ key: 'local', config: {}, selfRegistration: false, isEnabled: true, domainWhitelist: {v: []}, autoEnrollGroups: {v: []}, order: 0, strategyKey: 'local', displayName: 'Local' }) // Load editors + enable default await WIKI.models.editors.refreshEditorsFromDisk() await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown') // Load loggers await WIKI.models.loggers.refreshLoggersFromDisk() // Load renderers await WIKI.models.renderers.refreshRenderersFromDisk() // Load search engines + enable default await WIKI.models.searchEngines.refreshSearchEnginesFromDisk() await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db') // WIKI.telemetry.sendEvent('setup', 'install-loadedmodules') // Load storage targets await WIKI.models.storage.refreshTargetsFromDisk() // Create root administrator WIKI.logger.info('Creating root administrator...') const adminUser = await WIKI.models.users.query().insert({ email: req.body.adminEmail.toLowerCase(), provider: 'local', password: req.body.adminPassword, name: 'Administrator', locale: 'en', defaultEditor: 'markdown', tfaIsActive: false, isActive: true, isVerified: true }) await adminUser.$relatedQuery('groups').relate(adminGroup.id) // Create Guest account WIKI.logger.info('Creating guest account...') const guestUser = await WIKI.models.users.query().insert({ provider: 'local', email: 'guest@example.com', name: 'Guest', password: '', locale: 'en', defaultEditor: 'markdown', tfaIsActive: false, isSystem: true, isActive: true, isVerified: true }) await guestUser.$relatedQuery('groups').relate(guestGroup.id) if (adminUser.id !== 1 || guestUser.id !== 2) { throw new Error('Incorrect users auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.') } // Create site nav WIKI.logger.info('Creating default site navigation') await WIKI.models.navigation.query().insert({ key: 'site', config: [ { locale: 'en', items: [ { id: uuid(), icon: 'mdi-home', kind: 'link', label: 'Home', target: '/', targetType: 'home', visibilityMode: 'all', visibilityGroups: null } ] } ] }) WIKI.logger.info('Setup is complete!') // WIKI.telemetry.sendEvent('setup', 'install-completed') res.json({ ok: true, redirectPath: '/', redirectPort: WIKI.config.port }).end() if (WIKI.config.telemetry.isEnabled) { await WIKI.telemetry.sendInstanceEvent('INSTALL') } WIKI.config.setup = false WIKI.logger.info('Stopping Setup...') WIKI.server.destroy(() => { WIKI.logger.info('Setup stopped. Starting Wiki.js...') _.delay(() => { WIKI.kernel.bootMaster() }, 1000) }) } catch (err) { try { await WIKI.models.knex('settings').truncate() } catch (err) {} WIKI.telemetry.sendError(err) res.json({ ok: false, error: err.message }) } }) // ---------------------------------------- // Error handling // ---------------------------------------- app.use(function (req, res, next) { const err = new Error('Not Found') err.status = 404 next(err) }) app.use(function (err, req, res, next) { res.status(err.status || 500) res.send({ message: err.message, error: WIKI.IS_DEBUG ? err : {} }) WIKI.logger.error(err.message) WIKI.telemetry.sendError(err) }) // ---------------------------------------- // Start HTTP server // ---------------------------------------- WIKI.logger.info(`Starting HTTP server on port ${WIKI.config.port}...`) app.set('port', WIKI.config.port) WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`) WIKI.server = http.createServer(app) WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP) var openConnections = [] WIKI.server.on('connection', (conn) => { let key = conn.remoteAddress + ':' + conn.remotePort openConnections[key] = conn conn.on('close', () => { openConnections.splice(key, 1) }) }) WIKI.server.destroy = (cb) => { WIKI.server.close(cb) for (let key in openConnections) { openConnections[key].destroy() } } WIKI.server.on('error', (error) => { if (error.syscall !== 'listen') { throw error } switch (error.code) { case 'EACCES': WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!') return process.exit(1) case 'EADDRINUSE': WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!') return process.exit(1) default: throw error } }) WIKI.server.on('listening', () => { WIKI.logger.info('HTTP Server: [ RUNNING ]') WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻') WIKI.logger.info('') WIKI.logger.info(`Browse to http://YOUR-SERVER-IP:${WIKI.config.port}/ to complete setup!`) WIKI.logger.info('') WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺') }) } ================================================ FILE: server/templates/account-reset-pwd.html ================================================
    <%= preheadertext %>
    ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ 
    ================================================ FILE: server/templates/account-verify.html ================================================
    <%= preheadertext %>
    ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ 
    ================================================ FILE: server/templates/test.html ================================================
    <%= preheadertext %>
    ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ 
    ================================================ FILE: server/test/helpers/page.test.js ================================================ const { injectPageMetadata } = require('../../helpers/page') describe('helpers/page/injectPageMetadata', () => { const page = { title: 'PAGE TITLE', description: 'A PAGE', isPublished: true, updatedAt: new Date(), content: 'TEST CONTENT', createdAt: new Date('2019-01-01') } it('returns the page content by default when content type is unknown', () => { const expected = 'TEST CONTENT' const result = injectPageMetadata(page) expect(result).toEqual(expected) }) it('injects metadata for markdown contents', () => { const markdownPage = { ...page, contentType: 'markdown', editorKey: 'markdown' } const expected = `--- title: ${markdownPage.title} description: ${markdownPage.description} published: ${markdownPage.isPublished.toString()} date: ${markdownPage.updatedAt} tags:\x20 editor: ${markdownPage.editorKey} dateCreated: ${markdownPage.createdAt}\n--- TEST CONTENT` const result = injectPageMetadata(markdownPage) expect(result).toEqual(expected) }) it('injects metadata for html contents', () => { const htmlPage = { ...page, contentType: 'html', editorKey: 'html' } const expected = ` TEST CONTENT` const result = injectPageMetadata(htmlPage) expect(result).toEqual(expected) }) }) ================================================ FILE: server/themes/default/theme.yml ================================================ name: Default author: requarks.io site: https://wiki.requarks.io/ version: 1.0.0 requirements: minimum: '>= 2.0.0' maximum: '< 3.0.0' props: accentColor: type: String title: Accent Color hint: Color used in the sidebar navigation and other elements. order: 1 default: blue darken-2 control: color-material tocPosition: type: String title: Table of Contents Position hint: Select whether the table of contents is shown on the left, right or not at all. order: 2 default: left enum: - left - right - hidden ================================================ FILE: server/views/admin.pug ================================================ extends master.pug block body #root admin ================================================ FILE: server/views/editor.pug ================================================ extends master.pug block head if injectCode.css style(type='text/css')!= injectCode.css block body #root editor( :page-id=page.id locale=page.localeCode path=page.path title=page.title description=page.description :tags=page.tags :is-published=page.isPublished publish-start-date=page.publishStartDate publish-end-date=page.publishEndDate script-css=page.extra.css script-js=page.extra.js init-mode=page.mode init-editor=page.editorKey init-content=page.content checkout-date=page.updatedAt effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64') ) ================================================ FILE: server/views/error.pug ================================================ extends master.pug block body #root.is-fullscreen .app-error a(href='/') img(src='/_assets/svg/logo-wikijs.svg') strong Oops, something went wrong... span= message if error.stack pre: code #{error.stack} ================================================ FILE: server/views/history.pug ================================================ extends master.pug block head block body #root history( :page-id=page.id locale=page.localeCode path=page.path title=page.title description=page.description :tags=page.tags created-at=page.createdAt updated-at=page.updatedAt author-name=page.authorName :author-id=page.authorId :is-published=page.isPublished.toString() live-content=page.content effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64') ) ================================================ FILE: server/views/legacy/login.pug ================================================ extends master.pug block body #root .login-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '' + t('modernBrowser') + '', interpolation: { escapeValue: false } }) .login .login-dialog if err .login-error= err.message form(method='post', action='/login') h1= config.title select(name='strategy') each str in formStrategies option(value=str.key, selected)= str.title input(type='text', name='user', placeholder=t('auth:fields.emailUser')) input(type='password', name='pass', placeholder=t('auth:fields.password')) button(type='submit')= t('auth:actions.login') if socialStrategies.length .login-social h2= t('auth:orLoginUsingStrategy') each str in socialStrategies a.login-social-icon(href='/login/' + str.key, class=str.color) != str.icon ================================================ FILE: server/views/legacy/page.pug ================================================ extends master.pug block head if injectCode.css style(type='text/css')!= injectCode.css if injectCode.head != injectCode.head block body #root .header span.header-title= siteConfig.title span.header-deprecated!= t('outdatedBrowserWarning', { modernBrowser: '' + t('modernBrowser') + '', interpolation: { escapeValue: false } }) span.header-login if !isAuthenticated a(href='/login', title='Login') i.mdi.mdi-account-circle else a(href='/logout', title='Logout') i.mdi.mdi-logout .main .sidebar each navItem in sidebar if navItem.k === 'link' a.sidebar-link(href=navItem.t) i.mdi(class=navItem.c) span= navItem.l else if navItem.k === 'divider' .sidebar-divider else if navItem.k === 'header' .sidebar-title= navItem.l .main-container .page-header .page-header-left h1= page.title h2= page.description //- .page-header-right //- .page-header-right-title Last edited by //- .page-header-right-author= page.authorName //- .page-header-right-updated= page.updatedAt .page-contents.v-content .contents div!= page.render if page.toc.length .toc .toc-title= t('page.toc') each tocItem, tocIdx in page.toc a.toc-tile(href=tocItem.anchor) i.mdi.mdi-chevron-right span= tocItem.title if tocIdx < page.toc.length - 1 || tocItem.children.length .toc-divider each tocSubItem in tocItem.children a.toc-tile.inset(href=tocSubItem.anchor) i.mdi.mdi-chevron-right span= tocSubItem.title if tocIdx < page.toc.length - 1 .toc-divider.inset if injectCode.body != injectCode.body ================================================ FILE: server/views/login.pug ================================================ extends master.pug block body #root.is-fullscreen login( bg-url=bgUrl hide-local=hideLocal change-pwd-continuation-token=changePwdContinuationToken ) ================================================ FILE: server/views/new.pug ================================================ extends master.pug block body #root.is-fullscreen new-page(locale=locale, path=path) ================================================ FILE: server/views/notfound.pug ================================================ extends master.pug block body #root.is-fullscreen not-found ================================================ FILE: server/views/page.pug ================================================ extends master.pug block head if injectCode.css style(type='text/css')!= injectCode.css if injectCode.head != injectCode.head if config.features.featurePageComments != comments.head block body #root page( locale=page.localeCode path=page.path title=page.title description=page.description :tags=page.tags created-at=page.createdAt updated-at=page.updatedAt author-name=page.authorName :author-id=page.authorId editor=page.editorKey :is-published=page.isPublished.toString() toc=Buffer.from(page.toc).toString('base64') :page-id=page.id sidebar=Buffer.from(JSON.stringify(sidebar)).toString('base64') nav-mode=config.nav.mode comments-enabled=config.features.featurePageComments effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64') comments-external=comments.codeTemplate edit-shortcuts=Buffer.from(JSON.stringify(config.editShortcuts)).toString('base64') filename=pageFilename ) template(slot='contents') div!= page.render template(slot='comments') div!= comments.main if injectCode.body != injectCode.body if config.features.featurePageComments != comments.body ================================================ FILE: server/views/profile.pug ================================================ extends master.pug block body #root profile ================================================ FILE: server/views/register.pug ================================================ extends master.pug block body #root.is-fullscreen register ================================================ FILE: server/views/source.pug ================================================ extends master.pug block head block body #root page-source( :page-id=page.id locale=page.localeCode path=page.path :version-id=page.versionId version-date=page.versionDate effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64') ) code(v-pre)= page.content ================================================ FILE: server/views/tags.pug ================================================ extends master.pug block body #root tags ================================================ FILE: server/views/unauthorized.pug ================================================ extends master.pug block body #root.is-fullscreen unauthorized(action=action) ================================================ FILE: server/views/welcome.pug ================================================ extends master.pug block body #root.is-fullscreen welcome(locale=locale)